add client telmetry and fix missing openapi on prefault

This commit is contained in:
miloschwartz
2025-12-11 10:38:48 -05:00
parent 963468d7fa
commit f81909489a
5 changed files with 83 additions and 19 deletions

View File

@@ -2,9 +2,9 @@ import { PostHog } from "posthog-node";
import config from "./config"; import config from "./config";
import { getHostMeta } from "./hostMeta"; import { getHostMeta } from "./hostMeta";
import logger from "@server/logger"; 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 { 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 { APP_VERSION } from "./consts";
import crypto from "crypto"; import crypto from "crypto";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
@@ -25,7 +25,7 @@ class TelemetryClient {
return; return;
} }
if (build !== "oss") { if (build === "saas") {
return; return;
} }
@@ -41,14 +41,18 @@ class TelemetryClient {
this.client?.shutdown(); this.client?.shutdown();
}); });
this.sendStartupEvents().catch((err) => { this.sendStartupEvents()
logger.error("Failed to send startup telemetry:", err); .catch((err) => {
}); logger.error("Failed to send startup telemetry:", err);
})
.then(() => {
logger.debug("Successfully sent startup telemetry data");
});
this.startAnalyticsInterval(); this.startAnalyticsInterval();
logger.info( 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) { } else if (!this.enabled) {
logger.info( logger.info(
@@ -60,9 +64,13 @@ class TelemetryClient {
private startAnalyticsInterval() { private startAnalyticsInterval() {
this.intervalId = setInterval( this.intervalId = setInterval(
() => { () => {
this.collectAndSendAnalytics().catch((err) => { this.collectAndSendAnalytics()
logger.error("Failed to collect analytics:", err); .catch((err) => {
}); logger.error("Failed to collect analytics:", err);
})
.then(() => {
logger.debug("Successfully sent analytics data");
});
}, },
48 * 60 * 60 * 1000 48 * 60 * 60 * 1000
); );
@@ -99,9 +107,14 @@ class TelemetryClient {
const [resourcesCount] = await db const [resourcesCount] = await db
.select({ count: count() }) .select({ count: count() })
.from(resources); .from(resources);
const [clientsCount] = await db const [userDevicesCount] = await db
.select({ count: count() }) .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 [idpCount] = await db.select({ count: count() }).from(idp);
const [onlineSitesCount] = await db const [onlineSitesCount] = await db
.select({ count: count() }) .select({ count: count() })
@@ -146,6 +159,24 @@ class TelemetryClient {
const supporterKey = config.getSupporterData(); 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 { return {
numSites: sitesCount.count, numSites: sitesCount.count,
numUsers: usersCount.count, numUsers: usersCount.count,
@@ -153,7 +184,11 @@ class TelemetryClient {
numUsersOidc: usersOidcCount.count, numUsersOidc: usersOidcCount.count,
numOrganizations: orgsCount.count, numOrganizations: orgsCount.count,
numResources: resourcesCount.count, numResources: resourcesCount.count,
numClients: clientsCount.count, numPrivateResources: numPrivResources,
numPrivateResourceAliases: numPrivResourceAliases,
numPrivateResourceHosts: numPrivResourceHosts,
numUserDevices: userDevicesCount.count,
numMachineClients: machineClients.count,
numIdentityProviders: idpCount.count, numIdentityProviders: idpCount.count,
numSitesOnline: onlineSitesCount.count, numSitesOnline: onlineSitesCount.count,
resources: resourceDetails, resources: resourceDetails,
@@ -196,7 +231,7 @@ class TelemetryClient {
logger.debug("Sending enterprise startup telemetry payload:", { logger.debug("Sending enterprise startup telemetry payload:", {
payload payload
}); });
// this.client.capture(payload); this.client.capture(payload);
} }
if (build === "oss") { if (build === "oss") {
@@ -246,7 +281,12 @@ class TelemetryClient {
num_users_oidc: stats.numUsersOidc, num_users_oidc: stats.numUsersOidc,
num_organizations: stats.numOrganizations, num_organizations: stats.numOrganizations,
num_resources: stats.numResources, 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_identity_providers: stats.numIdentityProviders,
num_sites_online: stats.numSitesOnline, num_sites_online: stats.numSitesOnline,
num_resources_sso_enabled: stats.resources.filter( num_resources_sso_enabled: stats.resources.filter(

View File

@@ -34,7 +34,13 @@ export const queryAccessAuditLogsQuery = z.object({
error: "timeStart must be a valid ISO date string" error: "timeStart must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .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 timeEnd: z
.string() .string()
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {

View File

@@ -34,7 +34,13 @@ export const queryActionAuditLogsQuery = z.object({
error: "timeStart must be a valid ISO date string" error: "timeStart must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .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 timeEnd: z
.string() .string()
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {

View File

@@ -21,7 +21,13 @@ const queryAccessAuditLogsQuery = z.object({
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional() .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 timeEnd: z
.string() .string()
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {

View File

@@ -21,7 +21,13 @@ export const queryAccessAuditLogsQuery = z.object({
error: "timeStart must be a valid ISO date string" error: "timeStart must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .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 timeEnd: z
.string() .string()
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {