From f81909489aedb5bb4ab0d8f5d053e3c884fc2e93 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 11 Dec 2025 10:38:48 -0500 Subject: [PATCH] add client telmetry and fix missing openapi on prefault --- server/lib/telemetry.ts | 70 +++++++++++++++---- .../routers/auditLogs/queryAccessAuditLog.ts | 8 ++- .../routers/auditLogs/queryActionAuditLog.ts | 8 ++- .../auditLogs/queryRequestAnalytics.ts | 8 ++- .../routers/auditLogs/queryRequestAuditLog.ts | 8 ++- 5 files changed, 83 insertions(+), 19 deletions(-) diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts index 13ba1c95..fda59f39 100644 --- a/server/lib/telemetry.ts +++ b/server/lib/telemetry.ts @@ -2,9 +2,9 @@ import { PostHog } from "posthog-node"; import config from "./config"; import { getHostMeta } from "./hostMeta"; import logger from "@server/logger"; -import { apiKeys, db, roles } from "@server/db"; +import { apiKeys, db, roles, siteResources } from "@server/db"; import { sites, users, orgs, resources, clients, idp } from "@server/db"; -import { eq, count, notInArray, and } from "drizzle-orm"; +import { eq, count, notInArray, and, isNotNull, isNull } from "drizzle-orm"; import { APP_VERSION } from "./consts"; import crypto from "crypto"; import { UserType } from "@server/types/UserTypes"; @@ -25,7 +25,7 @@ class TelemetryClient { return; } - if (build !== "oss") { + if (build === "saas") { return; } @@ -41,14 +41,18 @@ class TelemetryClient { this.client?.shutdown(); }); - this.sendStartupEvents().catch((err) => { - logger.error("Failed to send startup telemetry:", err); - }); + this.sendStartupEvents() + .catch((err) => { + logger.error("Failed to send startup telemetry:", err); + }) + .then(() => { + logger.debug("Successfully sent startup telemetry data"); + }); this.startAnalyticsInterval(); logger.info( - "Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.pangolin.net/telemetry" + "Pangolin gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.pangolin.net/telemetry" ); } else if (!this.enabled) { logger.info( @@ -60,9 +64,13 @@ class TelemetryClient { private startAnalyticsInterval() { this.intervalId = setInterval( () => { - this.collectAndSendAnalytics().catch((err) => { - logger.error("Failed to collect analytics:", err); - }); + this.collectAndSendAnalytics() + .catch((err) => { + logger.error("Failed to collect analytics:", err); + }) + .then(() => { + logger.debug("Successfully sent analytics data"); + }); }, 48 * 60 * 60 * 1000 ); @@ -99,9 +107,14 @@ class TelemetryClient { const [resourcesCount] = await db .select({ count: count() }) .from(resources); - const [clientsCount] = await db + const [userDevicesCount] = await db .select({ count: count() }) - .from(clients); + .from(clients) + .where(isNotNull(clients.userId)); + const [machineClients] = await db + .select({ count: count() }) + .from(clients) + .where(isNull(clients.userId)); const [idpCount] = await db.select({ count: count() }).from(idp); const [onlineSitesCount] = await db .select({ count: count() }) @@ -146,6 +159,24 @@ class TelemetryClient { const supporterKey = config.getSupporterData(); + const allPrivateResources = await db.select().from(siteResources); + + const numPrivResources = allPrivateResources.length; + let numPrivResourceAliases = 0; + let numPrivResourceHosts = 0; + let numPrivResourceCidr = 0; + for (const res of allPrivateResources) { + if (res.mode === "host") { + numPrivResourceHosts += 1; + } else if (res.mode === "cidr") { + numPrivResourceCidr += 1; + } + + if (res.alias) { + numPrivResourceAliases += 1; + } + } + return { numSites: sitesCount.count, numUsers: usersCount.count, @@ -153,7 +184,11 @@ class TelemetryClient { numUsersOidc: usersOidcCount.count, numOrganizations: orgsCount.count, numResources: resourcesCount.count, - numClients: clientsCount.count, + numPrivateResources: numPrivResources, + numPrivateResourceAliases: numPrivResourceAliases, + numPrivateResourceHosts: numPrivResourceHosts, + numUserDevices: userDevicesCount.count, + numMachineClients: machineClients.count, numIdentityProviders: idpCount.count, numSitesOnline: onlineSitesCount.count, resources: resourceDetails, @@ -196,7 +231,7 @@ class TelemetryClient { logger.debug("Sending enterprise startup telemetry payload:", { payload }); - // this.client.capture(payload); + this.client.capture(payload); } if (build === "oss") { @@ -246,7 +281,12 @@ class TelemetryClient { num_users_oidc: stats.numUsersOidc, num_organizations: stats.numOrganizations, num_resources: stats.numResources, - num_clients: stats.numClients, + num_private_resources: stats.numPrivateResources, + num_private_resource_aliases: + stats.numPrivateResourceAliases, + num_private_resource_hosts: stats.numPrivateResourceHosts, + num_user_devices: stats.numUserDevices, + num_machine_clients: stats.numMachineClients, num_identity_providers: stats.numIdentityProviders, num_sites_online: stats.numSitesOnline, num_resources_sso_enabled: stats.resources.filter( diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index 7bc9968c..eb0cae5d 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -34,7 +34,13 @@ export const queryAccessAuditLogsQuery = z.object({ error: "timeStart must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) - .prefault(() => getSevenDaysAgo().toISOString()), + .prefault(() => getSevenDaysAgo().toISOString()) + .openapi({ + type: "string", + format: "date-time", + description: + "Start time as ISO date string (defaults to 7 days ago)" + }), timeEnd: z .string() .refine((val) => !isNaN(Date.parse(val)), { diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index 8c92aed2..518eb982 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -34,7 +34,13 @@ export const queryActionAuditLogsQuery = z.object({ error: "timeStart must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) - .prefault(() => getSevenDaysAgo().toISOString()), + .prefault(() => getSevenDaysAgo().toISOString()) + .openapi({ + type: "string", + format: "date-time", + description: + "Start time as ISO date string (defaults to 7 days ago)" + }), timeEnd: z .string() .refine((val) => !isNaN(Date.parse(val)), { diff --git a/server/routers/auditLogs/queryRequestAnalytics.ts b/server/routers/auditLogs/queryRequestAnalytics.ts index b439cbd6..a765f176 100644 --- a/server/routers/auditLogs/queryRequestAnalytics.ts +++ b/server/routers/auditLogs/queryRequestAnalytics.ts @@ -21,7 +21,13 @@ const queryAccessAuditLogsQuery = z.object({ }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .prefault(() => getSevenDaysAgo().toISOString()), + .prefault(() => getSevenDaysAgo().toISOString()) + .openapi({ + type: "string", + format: "date-time", + description: + "Start time as ISO date string (defaults to 7 days ago)" + }), timeEnd: z .string() .refine((val) => !isNaN(Date.parse(val)), { diff --git a/server/routers/auditLogs/queryRequestAuditLog.ts b/server/routers/auditLogs/queryRequestAuditLog.ts index 8776f21a..9cedec63 100644 --- a/server/routers/auditLogs/queryRequestAuditLog.ts +++ b/server/routers/auditLogs/queryRequestAuditLog.ts @@ -21,7 +21,13 @@ export const queryAccessAuditLogsQuery = z.object({ error: "timeStart must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) - .prefault(() => getSevenDaysAgo().toISOString()), + .prefault(() => getSevenDaysAgo().toISOString()) + .openapi({ + type: "string", + format: "date-time", + description: + "Start time as ISO date string (defaults to 7 days ago)" + }), timeEnd: z .string() .refine((val) => !isNaN(Date.parse(val)), {