Files
documenso/packages/lib/server-only/analytics/capture-server-event.ts
T
Lucas Smith ae4b56dd88 feat: optimise posthog usage and add error tracking (#3301)
Disable autocapture and person profiles for anonymous users, and
redact signing tokens from captured URLs.

Manually wire exception capture into the signing, editor and embed
flows with recipient/envelope context for debugging recipient-reported
issues.

Track activation events (webhooks, api tokens, direct links, embed
sessions) server-side with org attribution, and drop client events
already tracked in-app.
2026-08-27 11:11:51 +10:00

84 lines
2.1 KiB
TypeScript

import crypto from 'node:crypto';
import { PostHog } from 'posthog-node';
import { env } from '../../utils/env';
/**
* Server-side product analytics client.
*
* Uses the same PostHog project as the client-side integration
* (NEXT_PUBLIC_POSTHOG_KEY). Not to be confused with the self-hoster
* telemetry client which reports to a separate project.
*
* Events are sent directly to PostHog rather than through the `/ingest`
* reverse proxy used by the browser.
*/
const POSTHOG_HOST = 'https://eu.i.posthog.com';
let client: PostHog | null | undefined;
const getAnalyticsClient = () => {
if (client !== undefined) {
return client;
}
const apiKey = env('NEXT_PUBLIC_POSTHOG_KEY');
// Low volume events, flush immediately so nothing is lost on shutdown.
client = apiKey ? new PostHog(apiKey, { host: POSTHOG_HOST, flushAt: 1 }) : null;
return client;
};
type CaptureServerEventOptions = {
event: string;
/**
* The authenticated user this event belongs to, if any.
*
* Used as the distinct ID so server events line up with client-side
* identified users. Anonymous events (e.g. embed signer sessions) get a
* random distinct ID instead.
*/
userId?: number;
organisationId?: string;
teamId?: number;
properties?: Record<string, unknown>;
};
/**
* Capture a product analytics event from the server.
*
* No-ops when NEXT_PUBLIC_POSTHOG_KEY is not configured. Events are captured
* as anonymous events (no person profile processing) to keep usage costs low,
* cross-referencing is done via the `userId`, `organisationId` and `teamId`
* event properties.
*
* Never pass recipient tokens or document contents as properties.
*/
export const captureServerEvent = ({
event,
userId,
organisationId,
teamId,
properties = {},
}: CaptureServerEventOptions) => {
const posthog = getAnalyticsClient();
if (!posthog) {
return;
}
posthog.capture({
distinctId: userId ? String(userId) : `anon_${crypto.randomUUID()}`,
event,
properties: {
...properties,
userId,
organisationId,
teamId,
$process_person_profile: false,
},
});
};