返回 CodeWhale
posthog.ts
根目录 / telemetry-ingest / src / posthog.ts
1 /** Optional processor for the existing validated ingest; no second collector. */
2 import { CONSENT_VERSION, NOTICE_VERSION, SCHEMA_VERSION, type Batch } from "./schema";
3
4 export interface PostHogConfig {
5 /** Unset by default. Exactly one of the two regional HTTPS capture origins. */
6 POSTHOG_HOST?: string;
7 /** Project token, configured as a Worker secret only after activation approval. */
8 POSTHOG_PROJECT_TOKEN?: string;
9 /** Operator prerequisite, never code-level proof of Cloudflare header removal. */
10 POSTHOG_IP_SAFE_EGRESS_VERIFIED?: string;
11 }
12
13 export const POSTHOG_TIMEOUT_MS = 1_500;
14 const HOSTS = ["https://us.i.posthog.com", "https://eu.i.posthog.com"];
15
16 /** V2 keeps its opt-in contract; v3 identifies the disclosed opt-out policy. */
17 export async function deliverPostHog(batch: Batch, config: PostHogConfig): Promise<void> {
18 const explicitConsent = batch.schema_version === 2 && batch.consent_version === CONSENT_VERSION;
19 const disclosedPolicy = batch.schema_version === SCHEMA_VERSION && batch.notice_version === NOTICE_VERSION;
20 if (
21 !(explicitConsent || disclosedPolicy) ||
22 batch.events.length === 0 ||
23 config.POSTHOG_IP_SAFE_EGRESS_VERIFIED !== "true" ||
24 !config.POSTHOG_HOST || !HOSTS.includes(config.POSTHOG_HOST) ||
25 !config.POSTHOG_PROJECT_TOKEN || !/^phc_[A-Za-z0-9_-]{1,256}$/.test(config.POSTHOG_PROJECT_TOKEN)
26 ) return;
27
28 const { install_id, sent_at, events, ...envelope } = batch;
29 const body = JSON.stringify({
30 api_key: config.POSTHOG_PROJECT_TOKEN,
31 batch: events.map(({ event, ...properties }) => ({
32 event: `codewhale_${event}`,
33 timestamp: sent_at,
34 properties: {
35 ...envelope,
36 ...properties,
37 distinct_id: `codewhale:${install_id}`,
38 $process_person_profile: false,
39 $geoip_disable: true,
40 $ip: null,
41 },
42 })),
43 });
44 try {
45 // Copy no incoming headers or connection metadata. Cloudflare may still
46 // add platform headers; the operator guard requires a staging receipt for
47 // the actual egress path. Never follow a redirect carrying the token.
48 const response = await fetch(`${config.POSTHOG_HOST}/batch/`, {
49 method: "POST",
50 headers: { "content-type": "application/json" },
51 body,
52 redirect: "error",
53 credentials: "omit",
54 signal: AbortSignal.timeout(POSTHOG_TIMEOUT_MS),
55 });
56 // No retries, response parsing, shared queue, or log containing a token or payload.
57 void response.body?.cancel().catch(() => {});
58 } catch {
59 // Processor failure must not change the first-party ingest result.
60 }
61 }
62
62 lines TYPESCRIPT