better-ship 0.44.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../../src/infrastructure/posthog/server.ts"],"mappings":";;;;;;iBAiBiB;WACN;WACA;WACA;WACA,YAAY,SAAS;WACrB,UAAU,KACjB;aAGS,UAAU,KAAK,YAAY;;;;qBAK3B,mCAAmC;;WAMnC,SAAS;mBACD;;WALV,SAAS;UAEX;;SASA,OAAO,QAAQ,yBAAyB;;EAqB/C,SAAS,oBAAoB,aAAa;;EAO1C,MAAM,oBAAoB;;EAK1B,MACE,cACA,YAAY,mCACZ,QAAQ;;EAMV,eACE,cACA,YAAY,mCACZ,QAAQ;UAKF"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../../src/infrastructure/posthog/server.ts"],"mappings":";;;;;;iBAkBiB;WACN;WACA;WACA;WACA,YAAY,SAAS;WACrB,UAAU,KACjB;aAGS,UAAU,KAAK,YAAY;;;;qBAK3B,mCAAmC;;WAMnC,SAAS;mBACD;;WALV,SAAS;UAEX;;SASA,OAAO,QAAQ,yBAAyB;;EAqB/C,SAAS,oBAAoB,aAAa;;EAO1C,MAAM,oBAAoB;;EAK1B,MACE,cACA,YAAY,mCACZ,QAAQ;;EAMV,eACE,cACA,YAAY,mCACZ,QAAQ;UAKF"}
@@ -1,5 +1,6 @@
1
1
  import { nt as MissingConfigurationError } from "../core-CGj1ukXV.js";
2
2
  import { t as PostHogMetricsService } from "../metrics-CWv37yf9.js";
3
+ import { t as currentPostHogSession } from "../request-session-Qe_Q_u9G.js";
3
4
  import { v5, validate } from "uuid";
4
5
  import { PostHog } from "posthog-node";
5
6
 
@@ -60,7 +61,11 @@ var PostHogAnalyticsService = class PostHogAnalyticsService {
60
61
  this.capture(name, properties, target, "anonymous");
61
62
  }
62
63
  capture(name, eventProperties, target, profile) {
63
- const properties = { ...eventProperties };
64
+ const sessionId = currentPostHogSession();
65
+ const properties = sessionId ? {
66
+ $session_id: sessionId,
67
+ ...eventProperties
68
+ } : { ...eventProperties };
64
69
  const payload = {
65
70
  distinctId: target.distinctId,
66
71
  event: name,
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":["isUuid","uuidv5"],"sources":["../../src/infrastructure/posthog/server.ts"],"sourcesContent":["import { PostHog, type PostHogOptions } from 'posthog-node'\nimport { validate as isUuid, v5 as uuidv5 } from 'uuid'\n\nimport type { AnalyticsService } from '@/application'\nimport {\n MissingConfigurationError,\n type AnalyticsEventInput,\n type AnalyticsProperties,\n type AnalyticsTarget,\n} from '@/core'\n\nimport { PostHogMetricsService } from './metrics.ts'\n\n// Fixed forever: changing it changes every derived UUID, so PostHog would keep a repeat.\nconst EVENT_ID_NAMESPACE = '53d7af30-65cb-4176-b2c9-f1ed42367ab1'\n\n/** Deployment values and SDK options; the runtime owns delivery promises. */\nexport interface PostHogAnalyticsConfig {\n readonly apiKey: string\n readonly environment: string\n readonly source: string\n readonly waitUntil: (promise: Promise<unknown>) => void\n readonly options?: Omit<\n PostHogOptions,\n 'waitUntil' | 'flushAt' | 'flushInterval' | 'metrics' | 'disabled'\n > & {\n readonly metrics?: Omit<NonNullable<PostHogOptions['metrics']>, 'serviceName' | 'environment'>\n }\n}\n\n/** Worker analytics ported from Typist; disabled applications select Porte's NULL_ANALYTICS instead. */\nexport class PostHogAnalyticsService implements AnalyticsService {\n /** Metrics share this service's SDK client and delivery lifetime. */\n readonly metrics: PostHogMetricsService\n\n private constructor(\n /** Raw SDK access for integrations such as PostHog AI tracing. */\n readonly posthog: PostHog,\n private readonly waitUntil: (promise: Promise<unknown>) => void,\n ) {\n this.metrics = new PostHogMetricsService(posthog, waitUntil)\n }\n\n /** Construct once per Worker invocation; a blank key throws MissingConfigurationError. */\n static create(config: PostHogAnalyticsConfig): PostHogAnalyticsService {\n const apiKey = config.apiKey.trim()\n if (apiKey.length === 0) throw new MissingConfigurationError('POSTHOG_API_KEY')\n const client = new PostHog(apiKey, {\n host: 'https://us.i.posthog.com',\n ...config.options,\n flushAt: 1,\n flushInterval: 0,\n waitUntil: config.waitUntil,\n metrics: {\n ...config.options?.metrics,\n environment: config.environment,\n serviceName: config.source,\n },\n })\n // The SDK applies in-memory super properties synchronously before this promise resolves.\n void client.register({ environment: config.environment, source: config.source })\n return new PostHogAnalyticsService(client, config.waitUntil)\n }\n\n /** Schedule person properties; the SDK's waitUntil option protects queued delivery. */\n identify(distinctId: string, properties?: AnalyticsProperties): void {\n const payload: Parameters<PostHog['identify']>[0] = { distinctId }\n if (properties !== undefined) payload.properties = properties\n this.posthog.identify(payload)\n }\n\n /** Schedule an irreversible person merge; only alias IDs the application proves belong to one person. */\n alias(distinctId: string, previousDistinctId: string): void {\n this.waitUntil(this.posthog.aliasImmediate({ distinctId, alias: previousDistinctId }))\n }\n\n /** Schedule an event; without an event ID the SDK sets a new UUID and the capture time. */\n track(\n name: string,\n properties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n ): void {\n this.capture(name, properties, target, 'person')\n }\n\n /** Schedule an event without creating a person profile for an attempt or resource ID. */\n trackAnonymous(\n name: string,\n properties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n ): void {\n this.capture(name, properties, target, 'anonymous')\n }\n\n private capture(\n name: string,\n eventProperties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n profile: 'person' | 'anonymous',\n ): void {\n const properties = { ...eventProperties }\n const payload: Parameters<PostHog['captureImmediate']>[0] = {\n distinctId: target.distinctId,\n event: name,\n properties,\n }\n if (profile === 'anonymous')\n payload.properties = { ...properties, $process_person_profile: false }\n if (target.eventId !== undefined) {\n // PostHog drops a repeat only when its uuid and timestamp match the first event.\n payload.uuid = isUuid(target.eventId)\n ? target.eventId\n : uuidv5(target.eventId, EVENT_ID_NAMESPACE)\n payload.timestamp = new Date(target.occurredAt)\n }\n this.waitUntil(this.posthog.captureImmediate(payload))\n }\n}\n"],"mappings":";;;;;;AAcA,MAAM,qBAAqB;;AAiB3B,IAAa,0BAAb,MAAa,wBAAoD;CAMpD;CACQ;;CALnB,AAAS;CAET,AAAQ,YAEN,AAAS,SACT,AAAiB,WACjB;EAFS;EACQ;EAEjB,KAAK,UAAU,IAAI,sBAAsB,SAAS,SAAS;CAC7D;;CAGA,OAAO,OAAO,QAAyD;EACrE,MAAM,SAAS,OAAO,OAAO,KAAK;EAClC,IAAI,OAAO,WAAW,GAAG,MAAM,IAAI,0BAA0B,iBAAiB;EAC9E,MAAM,SAAS,IAAI,QAAQ,QAAQ;GACjC,MAAM;GACN,GAAG,OAAO;GACV,SAAS;GACT,eAAe;GACf,WAAW,OAAO;GAClB,SAAS;IACP,GAAG,OAAO,SAAS;IACnB,aAAa,OAAO;IACpB,aAAa,OAAO;GACtB;EACF,CAAC;EAED,AAAK,OAAO,SAAS;GAAE,aAAa,OAAO;GAAa,QAAQ,OAAO;EAAO,CAAC;EAC/E,OAAO,IAAI,wBAAwB,QAAQ,OAAO,SAAS;CAC7D;;CAGA,SAAS,YAAoB,YAAwC;EACnE,MAAM,UAA8C,EAAE,WAAW;EACjE,IAAI,eAAe,QAAW,QAAQ,aAAa;EACnD,KAAK,QAAQ,SAAS,OAAO;CAC/B;;CAGA,MAAM,YAAoB,oBAAkC;EAC1D,KAAK,UAAU,KAAK,QAAQ,eAAe;GAAE;GAAY,OAAO;EAAmB,CAAC,CAAC;CACvF;;CAGA,MACE,MACA,YACA,QACM;EACN,KAAK,QAAQ,MAAM,YAAY,QAAQ,QAAQ;CACjD;;CAGA,eACE,MACA,YACA,QACM;EACN,KAAK,QAAQ,MAAM,YAAY,QAAQ,WAAW;CACpD;CAEA,AAAQ,QACN,MACA,iBACA,QACA,SACM;EACN,MAAM,aAAa,EAAE,GAAG,gBAAgB;EACxC,MAAM,UAAsD;GAC1D,YAAY,OAAO;GACnB,OAAO;GACP;EACF;EACA,IAAI,YAAY,aACd,QAAQ,aAAa;GAAE,GAAG;GAAY,yBAAyB;EAAM;EACvE,IAAI,OAAO,YAAY,QAAW;GAEhC,QAAQ,OAAOA,SAAO,OAAO,OAAO,IAChC,OAAO,UACPC,GAAO,OAAO,SAAS,kBAAkB;GAC7C,QAAQ,YAAY,IAAI,KAAK,OAAO,UAAU;EAChD;EACA,KAAK,UAAU,KAAK,QAAQ,iBAAiB,OAAO,CAAC;CACvD;AACF"}
1
+ {"version":3,"file":"server.js","names":["isUuid","uuidv5"],"sources":["../../src/infrastructure/posthog/server.ts"],"sourcesContent":["import { PostHog, type PostHogOptions } from 'posthog-node'\nimport { validate as isUuid, v5 as uuidv5 } from 'uuid'\n\nimport type { AnalyticsService } from '@/application'\nimport {\n MissingConfigurationError,\n type AnalyticsEventInput,\n type AnalyticsProperties,\n type AnalyticsTarget,\n} from '@/core'\n\nimport { PostHogMetricsService } from './metrics.ts'\nimport { currentPostHogSession } from './request-session.ts'\n\n// Fixed forever: changing it changes every derived UUID, so PostHog would keep a repeat.\nconst EVENT_ID_NAMESPACE = '53d7af30-65cb-4176-b2c9-f1ed42367ab1'\n\n/** Deployment values and SDK options; the runtime owns delivery promises. */\nexport interface PostHogAnalyticsConfig {\n readonly apiKey: string\n readonly environment: string\n readonly source: string\n readonly waitUntil: (promise: Promise<unknown>) => void\n readonly options?: Omit<\n PostHogOptions,\n 'waitUntil' | 'flushAt' | 'flushInterval' | 'metrics' | 'disabled'\n > & {\n readonly metrics?: Omit<NonNullable<PostHogOptions['metrics']>, 'serviceName' | 'environment'>\n }\n}\n\n/** Worker analytics ported from Typist; disabled applications select Porte's NULL_ANALYTICS instead. */\nexport class PostHogAnalyticsService implements AnalyticsService {\n /** Metrics share this service's SDK client and delivery lifetime. */\n readonly metrics: PostHogMetricsService\n\n private constructor(\n /** Raw SDK access for integrations such as PostHog AI tracing. */\n readonly posthog: PostHog,\n private readonly waitUntil: (promise: Promise<unknown>) => void,\n ) {\n this.metrics = new PostHogMetricsService(posthog, waitUntil)\n }\n\n /** Construct once per Worker invocation; a blank key throws MissingConfigurationError. */\n static create(config: PostHogAnalyticsConfig): PostHogAnalyticsService {\n const apiKey = config.apiKey.trim()\n if (apiKey.length === 0) throw new MissingConfigurationError('POSTHOG_API_KEY')\n const client = new PostHog(apiKey, {\n host: 'https://us.i.posthog.com',\n ...config.options,\n flushAt: 1,\n flushInterval: 0,\n waitUntil: config.waitUntil,\n metrics: {\n ...config.options?.metrics,\n environment: config.environment,\n serviceName: config.source,\n },\n })\n // The SDK applies in-memory super properties synchronously before this promise resolves.\n void client.register({ environment: config.environment, source: config.source })\n return new PostHogAnalyticsService(client, config.waitUntil)\n }\n\n /** Schedule person properties; the SDK's waitUntil option protects queued delivery. */\n identify(distinctId: string, properties?: AnalyticsProperties): void {\n const payload: Parameters<PostHog['identify']>[0] = { distinctId }\n if (properties !== undefined) payload.properties = properties\n this.posthog.identify(payload)\n }\n\n /** Schedule an irreversible person merge; only alias IDs the application proves belong to one person. */\n alias(distinctId: string, previousDistinctId: string): void {\n this.waitUntil(this.posthog.aliasImmediate({ distinctId, alias: previousDistinctId }))\n }\n\n /** Schedule an event; without an event ID the SDK sets a new UUID and the capture time. */\n track(\n name: string,\n properties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n ): void {\n this.capture(name, properties, target, 'person')\n }\n\n /** Schedule an event without creating a person profile for an attempt or resource ID. */\n trackAnonymous(\n name: string,\n properties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n ): void {\n this.capture(name, properties, target, 'anonymous')\n }\n\n private capture(\n name: string,\n eventProperties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n profile: 'person' | 'anonymous',\n ): void {\n const sessionId = currentPostHogSession()\n // An application's own `$session_id` wins over the request's.\n const properties = sessionId\n ? { $session_id: sessionId, ...eventProperties }\n : { ...eventProperties }\n const payload: Parameters<PostHog['captureImmediate']>[0] = {\n distinctId: target.distinctId,\n event: name,\n properties,\n }\n if (profile === 'anonymous')\n payload.properties = { ...properties, $process_person_profile: false }\n if (target.eventId !== undefined) {\n // PostHog drops a repeat only when its uuid and timestamp match the first event.\n payload.uuid = isUuid(target.eventId)\n ? target.eventId\n : uuidv5(target.eventId, EVENT_ID_NAMESPACE)\n payload.timestamp = new Date(target.occurredAt)\n }\n this.waitUntil(this.posthog.captureImmediate(payload))\n }\n}\n"],"mappings":";;;;;;;AAeA,MAAM,qBAAqB;;AAiB3B,IAAa,0BAAb,MAAa,wBAAoD;CAMpD;CACQ;;CALnB,AAAS;CAET,AAAQ,YAEN,AAAS,SACT,AAAiB,WACjB;EAFS;EACQ;EAEjB,KAAK,UAAU,IAAI,sBAAsB,SAAS,SAAS;CAC7D;;CAGA,OAAO,OAAO,QAAyD;EACrE,MAAM,SAAS,OAAO,OAAO,KAAK;EAClC,IAAI,OAAO,WAAW,GAAG,MAAM,IAAI,0BAA0B,iBAAiB;EAC9E,MAAM,SAAS,IAAI,QAAQ,QAAQ;GACjC,MAAM;GACN,GAAG,OAAO;GACV,SAAS;GACT,eAAe;GACf,WAAW,OAAO;GAClB,SAAS;IACP,GAAG,OAAO,SAAS;IACnB,aAAa,OAAO;IACpB,aAAa,OAAO;GACtB;EACF,CAAC;EAED,AAAK,OAAO,SAAS;GAAE,aAAa,OAAO;GAAa,QAAQ,OAAO;EAAO,CAAC;EAC/E,OAAO,IAAI,wBAAwB,QAAQ,OAAO,SAAS;CAC7D;;CAGA,SAAS,YAAoB,YAAwC;EACnE,MAAM,UAA8C,EAAE,WAAW;EACjE,IAAI,eAAe,QAAW,QAAQ,aAAa;EACnD,KAAK,QAAQ,SAAS,OAAO;CAC/B;;CAGA,MAAM,YAAoB,oBAAkC;EAC1D,KAAK,UAAU,KAAK,QAAQ,eAAe;GAAE;GAAY,OAAO;EAAmB,CAAC,CAAC;CACvF;;CAGA,MACE,MACA,YACA,QACM;EACN,KAAK,QAAQ,MAAM,YAAY,QAAQ,QAAQ;CACjD;;CAGA,eACE,MACA,YACA,QACM;EACN,KAAK,QAAQ,MAAM,YAAY,QAAQ,WAAW;CACpD;CAEA,AAAQ,QACN,MACA,iBACA,QACA,SACM;EACN,MAAM,YAAY,sBAAsB;EAExC,MAAM,aAAa,YACf;GAAE,aAAa;GAAW,GAAG;EAAgB,IAC7C,EAAE,GAAG,gBAAgB;EACzB,MAAM,UAAsD;GAC1D,YAAY,OAAO;GACnB,OAAO;GACP;EACF;EACA,IAAI,YAAY,aACd,QAAQ,aAAa;GAAE,GAAG;GAAY,yBAAyB;EAAM;EACvE,IAAI,OAAO,YAAY,QAAW;GAEhC,QAAQ,OAAOA,SAAO,OAAO,OAAO,IAChC,OAAO,UACPC,GAAO,OAAO,SAAS,kBAAkB;GAC7C,QAAQ,YAAY,IAAI,KAAK,OAAO,UAAU;EAChD;EACA,KAAK,UAAU,KAAK,QAAQ,iBAAiB,OAAO,CAAC;CACvD;AACF"}
@@ -0,0 +1,9 @@
1
+ //#region src/infrastructure/posthog/tanstack.d.ts
2
+ /**
3
+ * Link Worker events to the browser session that sent the request.
4
+ * Register it in `requestMiddleware`. Each `PostHogAnalyticsService` event in that request then
5
+ * gets `$session_id`. A request without the header, and queue, cron, and Workflow code, add nothing.
6
+ */
7
+ export declare const posthogRequestMiddleware: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
8
+ //#endregion
9
+ //# sourceMappingURL=tanstack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tanstack.d.ts","names":[],"sources":["../../src/infrastructure/posthog/tanstack.ts"],"mappings":";;;;;;qBAUa,0DAAwB"}
@@ -0,0 +1,19 @@
1
+ import { createMiddleware } from "@tanstack/react-start";
2
+
3
+ //#region src/infrastructure/posthog/tanstack.ts
4
+ const SESSION_HEADER = "x-posthog-session-id";
5
+ /**
6
+ * Link Worker events to the browser session that sent the request.
7
+ * Register it in `requestMiddleware`. Each `PostHogAnalyticsService` event in that request then
8
+ * gets `$session_id`. A request without the header, and queue, cron, and Workflow code, add nothing.
9
+ */
10
+ const posthogRequestMiddleware = createMiddleware({ type: "request" }).server(async ({ request, next }) => {
11
+ const sessionId = request.headers.get(SESSION_HEADER)?.trim();
12
+ if (!sessionId) return next();
13
+ const { runWithPostHogSession } = await import("../request-session-D1r9ZvcS.js");
14
+ return runWithPostHogSession(sessionId, () => next());
15
+ });
16
+
17
+ //#endregion
18
+ export { posthogRequestMiddleware };
19
+ //# sourceMappingURL=tanstack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tanstack.js","names":[],"sources":["../../src/infrastructure/posthog/tanstack.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start'\n\n// posthog-js sends this header when the browser sets `tracing_headers` for the application host.\nconst SESSION_HEADER = 'x-posthog-session-id'\n\n/**\n * Link Worker events to the browser session that sent the request.\n * Register it in `requestMiddleware`. Each `PostHogAnalyticsService` event in that request then\n * gets `$session_id`. A request without the header, and queue, cron, and Workflow code, add nothing.\n */\nexport const posthogRequestMiddleware = createMiddleware({ type: 'request' }).server(\n async ({ request, next }) => {\n const sessionId = request.headers.get(SESSION_HEADER)?.trim()\n if (!sessionId) return next()\n // The browser evaluates this module, so the Node async scope loads inside `.server()`.\n const { runWithPostHogSession } = await import('./request-session.ts')\n return runWithPostHogSession(sessionId, () => next())\n },\n)\n"],"mappings":";;;AAGA,MAAM,iBAAiB;;;;;;AAOvB,MAAa,2BAA2B,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OAC5E,OAAO,EAAE,SAAS,WAAW;CAC3B,MAAM,YAAY,QAAQ,QAAQ,IAAI,cAAc,CAAC,EAAE,KAAK;CAC5D,IAAI,CAAC,WAAW,OAAO,KAAK;CAE5B,MAAM,EAAE,0BAA0B,MAAM,OAAO;CAC/C,OAAO,sBAAsB,iBAAiB,KAAK,CAAC;AACtD,CACF"}
@@ -0,0 +1,3 @@
1
+ import { n as runWithPostHogSession, t as currentPostHogSession } from "./request-session-Qe_Q_u9G.js";
2
+
3
+ export { runWithPostHogSession };
@@ -0,0 +1,16 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ //#region src/infrastructure/posthog/request-session.ts
4
+ const storage = new AsyncLocalStorage();
5
+ /** Run the rest of one request with the browser's PostHog session ID. */
6
+ function runWithPostHogSession(sessionId, work) {
7
+ return storage.run(sessionId, work);
8
+ }
9
+ /** The current request's PostHog session ID, or undefined outside such a request. */
10
+ function currentPostHogSession() {
11
+ return storage.getStore();
12
+ }
13
+
14
+ //#endregion
15
+ export { runWithPostHogSession as n, currentPostHogSession as t };
16
+ //# sourceMappingURL=request-session-Qe_Q_u9G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-session-Qe_Q_u9G.js","names":[],"sources":["../src/infrastructure/posthog/request-session.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\n\n// One async scope per request keeps each browser session ID apart from concurrent requests.\nconst storage = new AsyncLocalStorage<string>()\n\n/** Run the rest of one request with the browser's PostHog session ID. */\nexport function runWithPostHogSession<T>(sessionId: string, work: () => T): T {\n return storage.run(sessionId, work)\n}\n\n/** The current request's PostHog session ID, or undefined outside such a request. */\nexport function currentPostHogSession(): string | undefined {\n return storage.getStore()\n}\n"],"mappings":";;;AAGA,MAAM,UAAU,IAAI,kBAA0B;;AAG9C,SAAgB,sBAAyB,WAAmB,MAAkB;CAC5E,OAAO,QAAQ,IAAI,WAAW,IAAI;AACpC;;AAGA,SAAgB,wBAA4C;CAC1D,OAAO,QAAQ,SAAS;AAC1B"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "better-ship",
4
- "version": "0.44.0",
4
+ "version": "0.45.0",
5
5
  "description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -52,6 +52,7 @@
52
52
  "./posthog/metrics": "./dist/posthog/metrics.js",
53
53
  "./posthog/proxy": "./dist/posthog/proxy.js",
54
54
  "./posthog/server": "./dist/posthog/server.js",
55
+ "./posthog/tanstack": "./dist/posthog/tanstack.js",
55
56
  "./resend": "./dist/resend.js",
56
57
  "./sentry/client": "./dist/sentry/client.js",
57
58
  "./sentry/server": "./dist/sentry/server.js",
@@ -0,0 +1,14 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+
3
+ // One async scope per request keeps each browser session ID apart from concurrent requests.
4
+ const storage = new AsyncLocalStorage<string>()
5
+
6
+ /** Run the rest of one request with the browser's PostHog session ID. */
7
+ export function runWithPostHogSession<T>(sessionId: string, work: () => T): T {
8
+ return storage.run(sessionId, work)
9
+ }
10
+
11
+ /** The current request's PostHog session ID, or undefined outside such a request. */
12
+ export function currentPostHogSession(): string | undefined {
13
+ return storage.getStore()
14
+ }
@@ -10,6 +10,7 @@ import {
10
10
  } from '@/core'
11
11
 
12
12
  import { PostHogMetricsService } from './metrics.ts'
13
+ import { currentPostHogSession } from './request-session.ts'
13
14
 
14
15
  // Fixed forever: changing it changes every derived UUID, so PostHog would keep a repeat.
15
16
  const EVENT_ID_NAMESPACE = '53d7af30-65cb-4176-b2c9-f1ed42367ab1'
@@ -98,7 +99,11 @@ export class PostHogAnalyticsService implements AnalyticsService {
98
99
  target: AnalyticsTarget,
99
100
  profile: 'person' | 'anonymous',
100
101
  ): void {
101
- const properties = { ...eventProperties }
102
+ const sessionId = currentPostHogSession()
103
+ // An application's own `$session_id` wins over the request's.
104
+ const properties = sessionId
105
+ ? { $session_id: sessionId, ...eventProperties }
106
+ : { ...eventProperties }
102
107
  const payload: Parameters<PostHog['captureImmediate']>[0] = {
103
108
  distinctId: target.distinctId,
104
109
  event: name,
@@ -0,0 +1,19 @@
1
+ import { createMiddleware } from '@tanstack/react-start'
2
+
3
+ // posthog-js sends this header when the browser sets `tracing_headers` for the application host.
4
+ const SESSION_HEADER = 'x-posthog-session-id'
5
+
6
+ /**
7
+ * Link Worker events to the browser session that sent the request.
8
+ * Register it in `requestMiddleware`. Each `PostHogAnalyticsService` event in that request then
9
+ * gets `$session_id`. A request without the header, and queue, cron, and Workflow code, add nothing.
10
+ */
11
+ export const posthogRequestMiddleware = createMiddleware({ type: 'request' }).server(
12
+ async ({ request, next }) => {
13
+ const sessionId = request.headers.get(SESSION_HEADER)?.trim()
14
+ if (!sessionId) return next()
15
+ // The browser evaluates this module, so the Node async scope loads inside `.server()`.
16
+ const { runWithPostHogSession } = await import('./request-session.ts')
17
+ return runWithPostHogSession(sessionId, () => next())
18
+ },
19
+ )