swish-recorder 0.2.0 → 0.2.1

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.
package/dist/index.cjs CHANGED
@@ -25,6 +25,17 @@ __export(index_exports, {
25
25
  module.exports = __toCommonJS(index_exports);
26
26
  var API_HOST = "https://api.swish.is";
27
27
  var PREFIX = "[swish-recorder]";
28
+ var VERSION = "0.2.1";
29
+ function getToken(posthog) {
30
+ var _a;
31
+ const token = (_a = posthog.config) == null ? void 0 : _a.token;
32
+ if (!token) throw new Error(`${PREFIX} PostHog instance has no config.token`);
33
+ return token;
34
+ }
35
+ function getBeforeSend(posthog) {
36
+ var _a;
37
+ return (_a = posthog.config) == null ? void 0 : _a.before_send;
38
+ }
28
39
  var SwishRecorder = class _SwishRecorder {
29
40
  constructor(config) {
30
41
  this.config = config;
@@ -81,7 +92,8 @@ var SwishRecorder = class _SwishRecorder {
81
92
  window_id: props.$window_id,
82
93
  snapshot_data: props.$snapshot_data,
83
94
  distinct_id: props.distinct_id,
84
- timestamp: ts
95
+ timestamp: ts,
96
+ sdk_version: VERSION
85
97
  })
86
98
  }).then((res) => {
87
99
  if (debug) {
@@ -104,7 +116,7 @@ var SwishRecorder = class _SwishRecorder {
104
116
  return event;
105
117
  };
106
118
  const bind = (posthog) => {
107
- token = posthog.config.token;
119
+ token = getToken(posthog);
108
120
  log(`bind \u2014 token=${token.slice(0, 12)}\u2026`);
109
121
  if (pendingSnapshots.length > 0) {
110
122
  log(`flushing ${pendingSnapshots.length} buffered snapshots`);
@@ -126,7 +138,7 @@ var SwishRecorder = class _SwishRecorder {
126
138
  JSON.stringify({
127
139
  session_id: sessionId,
128
140
  reason,
129
- token: this.config.posthog.config.token
141
+ token: getToken(this.config.posthog)
130
142
  })
131
143
  ],
132
144
  { type: "application/json" }
@@ -141,14 +153,15 @@ var SwishRecorder = class _SwishRecorder {
141
153
  method: "POST",
142
154
  headers: {
143
155
  "Content-Type": "application/json",
144
- "X-PostHog-Token": this.config.posthog.config.token
156
+ "X-PostHog-Token": getToken(this.config.posthog)
145
157
  },
146
158
  body: JSON.stringify({
147
159
  session_id: sessionId,
148
160
  window_id: windowId,
149
161
  snapshot_data: snapshotData,
150
162
  distinct_id: distinctId,
151
- timestamp: Date.now()
163
+ timestamp: Date.now(),
164
+ sdk_version: VERSION
152
165
  })
153
166
  }).then((res) => {
154
167
  if (this.config.debug) {
@@ -164,12 +177,14 @@ var SwishRecorder = class _SwishRecorder {
164
177
  }
165
178
  _setup() {
166
179
  const { posthog } = this.config;
167
- const token = posthog.config.token;
168
- this._log(`init \u2014 token=${token.slice(0, 12)}\u2026`);
169
- if (!token) {
180
+ let token;
181
+ try {
182
+ token = getToken(posthog);
183
+ } catch (e) {
170
184
  this._warn("no PostHog token found \u2014 recording disabled");
171
185
  return;
172
186
  }
187
+ this._log(`init \u2014 token=${token.slice(0, 12)}\u2026`);
173
188
  if (!this.config._skipBeforeSend) {
174
189
  const snapshotHandler = (event) => {
175
190
  if ((event == null ? void 0 : event.event) === "$snapshot") {
@@ -185,7 +200,7 @@ var SwishRecorder = class _SwishRecorder {
185
200
  }
186
201
  return event;
187
202
  };
188
- const existing = posthog.config.before_send;
203
+ const existing = getBeforeSend(posthog);
189
204
  this._log(
190
205
  `before_send \u2014 existing handler: ${existing ? Array.isArray(existing) ? `array(${existing.length})` : "function" : "none"}`
191
206
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Minimal PostHog interface — no posthog-js dependency needed\ninterface PostHogLike {\n set_config(config: { before_send?: unknown }): void;\n onSessionId(\n callback: (\n sessionId: string,\n windowId: string | null | undefined,\n changeReason?: {\n activityTimeout?: boolean;\n sessionPastMaximumLength?: boolean;\n },\n ) => void,\n ): () => void;\n readonly config: {\n token: string;\n before_send?: unknown;\n };\n}\n\ntype BeforeSendFn = (\n event: { event: string; properties: Record<string, unknown> } | null,\n) => { event: string; properties: Record<string, unknown> } | null;\n\nconst API_HOST = \"https://api.swish.is\";\nconst PREFIX = \"[swish-recorder]\";\n\nexport interface SwishRecorderConfig {\n /** PostHog instance from your app — token is read automatically. */\n posthog: PostHogLike;\n /** Enable console logging for debugging. */\n debug?: boolean;\n /** Override the Swish API URL. Useful for local development (e.g. \"http://localhost:8000\"). */\n apiHost?: string;\n /** @internal Skip before_send registration (already handled by beforeSendHandler). */\n _skipBeforeSend?: boolean;\n}\n\nexport class SwishRecorder {\n private currentSessionId: string | null = null;\n private unsubscribeSessionId: (() => void) | null = null;\n private handleUnload: (() => void) | null = null;\n\n private readonly apiHost: string;\n\n private constructor(private readonly config: SwishRecorderConfig) {\n this.apiHost = config.apiHost ?? API_HOST;\n }\n\n private _log(...args: unknown[]): void {\n if (this.config.debug) console.log(PREFIX, ...args);\n }\n\n private _warn(...args: unknown[]): void {\n if (this.config.debug) console.warn(PREFIX, ...args);\n }\n\n static init(config: SwishRecorderConfig): SwishRecorder {\n const instance = new SwishRecorder(config);\n instance._setup();\n return instance;\n }\n\n /**\n * Create a `before_send` handler to pass directly into PostHog's config.\n * This ensures snapshot interception is active from the very first event,\n * avoiding the race condition where PostHog starts recording from persisted\n * config before `SwishRecorder.init()` can hook in.\n *\n * Usage:\n * const swish = SwishRecorder.beforeSendHandler({ apiHost: '...' })\n * posthog.init(token, { before_send: swish.handler })\n * // later, after posthog is ready:\n * swish.bind(posthog) // starts session lifecycle tracking\n */\n static beforeSendHandler(opts?: {\n apiHost?: string;\n debug?: boolean;\n }): { handler: BeforeSendFn; bind: (posthog: PostHogLike) => SwishRecorder } {\n const apiHost = opts?.apiHost ?? API_HOST;\n const debug = opts?.debug ?? false;\n let token: string | null = null;\n let pendingSnapshots: { props: Record<string, unknown>; ts: number }[] = [];\n\n const log = (...args: unknown[]) => {\n if (debug) console.log(PREFIX, ...args);\n };\n const warn = (...args: unknown[]) => {\n if (debug) console.warn(PREFIX, ...args);\n };\n\n const sendSnapshot = (props: Record<string, unknown>, ts: number) => {\n fetch(`${apiHost}/replays/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-PostHog-Token\": token!,\n },\n body: JSON.stringify({\n session_id: props.$session_id,\n window_id: props.$window_id,\n snapshot_data: props.$snapshot_data,\n distinct_id: props.distinct_id,\n timestamp: ts,\n }),\n })\n .then((res) => {\n if (debug) {\n if (res.ok) log(`ingest OK — ${res.status}`);\n else warn(`ingest failed — ${res.status} ${res.statusText}`);\n }\n })\n .catch((err) => warn(\"ingest error —\", err));\n };\n\n const handler: BeforeSendFn = (event) => {\n if (event?.event === \"$snapshot\") {\n const props = event.properties;\n const ts = Date.now();\n if (token) {\n sendSnapshot(props, ts);\n } else {\n // Buffer until bind() provides the token\n pendingSnapshots.push({ props, ts });\n log(`buffered snapshot (pre-bind) — ${pendingSnapshots.length} pending`);\n }\n }\n return event;\n };\n\n const bind = (posthog: PostHogLike): SwishRecorder => {\n token = posthog.config.token;\n log(`bind — token=${token.slice(0, 12)}…`);\n\n // Flush any snapshots captured before bind\n if (pendingSnapshots.length > 0) {\n log(`flushing ${pendingSnapshots.length} buffered snapshots`);\n for (const { props, ts } of pendingSnapshots) {\n sendSnapshot(props, ts);\n }\n pendingSnapshots = [];\n }\n\n return SwishRecorder.init({ posthog, apiHost, debug, _skipBeforeSend: true });\n };\n\n return { handler, bind };\n }\n\n private _sendSessionEnd(sessionId: string, reason: string): void {\n this._log(`session end — session=${sessionId} reason=${reason}`);\n navigator.sendBeacon(\n `${this.apiHost}/replays/session-end`,\n new Blob(\n [\n JSON.stringify({\n session_id: sessionId,\n reason,\n token: this.config.posthog.config.token,\n }),\n ],\n { type: \"application/json\" },\n ),\n );\n }\n\n private _ingestSnapshot(\n sessionId: string,\n windowId: string,\n snapshotData: unknown,\n distinctId: string,\n ): void {\n this._log(\n `ingesting snapshot — session=${sessionId} window=${windowId} distinct_id=${distinctId}`,\n );\n fetch(`${this.apiHost}/replays/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-PostHog-Token\": this.config.posthog.config.token,\n },\n body: JSON.stringify({\n session_id: sessionId,\n window_id: windowId,\n snapshot_data: snapshotData,\n distinct_id: distinctId,\n timestamp: Date.now(),\n }),\n })\n .then((res) => {\n if (this.config.debug) {\n if (res.ok) {\n this._log(`ingest OK — ${res.status}`);\n } else {\n this._warn(`ingest failed — ${res.status} ${res.statusText}`);\n }\n }\n })\n .catch((err) => {\n this._warn(\"ingest error —\", err);\n });\n }\n\n private _setup(): void {\n const { posthog } = this.config;\n const token = posthog.config.token;\n\n this._log(`init — token=${token.slice(0, 12)}…`);\n\n if (!token) {\n this._warn(\"no PostHog token found — recording disabled\");\n return;\n }\n\n // 1. Intercept $snapshot events via before_send (non-destructive append)\n // Skip if already registered via beforeSendHandler()\n if (!this.config._skipBeforeSend) {\n const snapshotHandler: BeforeSendFn = (event) => {\n if (event?.event === \"$snapshot\") {\n const props = event.properties;\n this._ingestSnapshot(\n props.$session_id as string,\n props.$window_id as string,\n props.$snapshot_data,\n props.distinct_id as string,\n );\n } else if (this.config.debug && event) {\n this._log(`before_send passthrough — event=${event.event}`);\n }\n return event;\n };\n\n const existing = posthog.config.before_send;\n this._log(\n `before_send — existing handler: ${existing ? (Array.isArray(existing) ? `array(${(existing as unknown[]).length})` : \"function\") : \"none\"}`,\n );\n posthog.set_config({\n before_send: existing\n ? Array.isArray(existing)\n ? [...(existing as BeforeSendFn[]), snapshotHandler]\n : [existing as BeforeSendFn, snapshotHandler]\n : snapshotHandler,\n });\n } else {\n this._log(\"before_send — already registered via beforeSendHandler()\");\n }\n\n // 2. Session rotation signal — fires on idle timeout or max session length\n this.unsubscribeSessionId = posthog.onSessionId(\n (newSessionId, _windowId, changeReason) => {\n this._log(\n `session ID changed — new=${newSessionId} reason=${JSON.stringify(changeReason ?? null)}`,\n );\n if (this.currentSessionId && changeReason) {\n const reason = changeReason.activityTimeout\n ? \"activity_timeout\"\n : changeReason.sessionPastMaximumLength\n ? \"session_length\"\n : null;\n if (reason) this._sendSessionEnd(this.currentSessionId, reason);\n }\n this.currentSessionId = newSessionId;\n },\n );\n\n // 3. Tab close — sendBeacon survives page unload, fetch does not\n this.handleUnload = () => {\n if (this.currentSessionId)\n this._sendSessionEnd(this.currentSessionId, \"tab_close\");\n };\n window.addEventListener(\"beforeunload\", this.handleUnload);\n\n this._log(\"ready\");\n }\n\n destroy(): void {\n this._log(\"destroy\");\n this.unsubscribeSessionId?.();\n if (this.handleUnload) {\n window.removeEventListener(\"beforeunload\", this.handleUnload);\n }\n this.unsubscribeSessionId = null;\n this.handleUnload = null;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Minimal PostHog interface — no posthog-js dependency needed.\n//\n// posthog-js types the `loaded` callback parameter as\n// `PostHogInterface = Omit<PostHog, 'config' | 'init'>`, which strips\n// `config` from the type even though the runtime object always has it.\n// We keep `config` optional here so users can pass either the full\n// PostHog instance or the `loaded` callback parameter without type errors.\n// At runtime we assert `config` exists — it always does.\ninterface PostHogLike {\n set_config(config: { before_send?: unknown }): void;\n onSessionId(\n callback: (\n sessionId: string,\n windowId: string | null | undefined,\n changeReason?: {\n noSessionId?: boolean;\n activityTimeout?: boolean;\n sessionPastMaximumLength?: boolean;\n },\n ) => void,\n ): () => void;\n config?: {\n token: string;\n before_send?: unknown;\n };\n}\n\n/**\n * Compatible with posthog-js BeforeSendFn / CaptureResult.\n * We use `any` for the public handler type so it's assignable to PostHog's\n * `before_send` without users needing type casts — the actual shapes evolve\n * across posthog-js versions and we only inspect `event` + `properties`.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype BeforeSendFn = (event: any) => any;\n\nconst API_HOST = \"https://api.swish.is\";\nconst PREFIX = \"[swish-recorder]\";\nconst VERSION = \"0.2.1\";\n\n/** Read posthog.config.token — always exists at runtime even when typed away. */\nfunction getToken(posthog: PostHogLike): string {\n const token = (posthog as { config?: { token?: string } }).config?.token;\n if (!token) throw new Error(`${PREFIX} PostHog instance has no config.token`);\n return token;\n}\n\n/** Read posthog.config.before_send — may be undefined. */\nfunction getBeforeSend(posthog: PostHogLike): unknown {\n return (posthog as { config?: { before_send?: unknown } }).config\n ?.before_send;\n}\n\nexport interface SwishRecorderConfig {\n /** PostHog instance from your app — token is read automatically. */\n posthog: PostHogLike;\n /** Enable console logging for debugging. */\n debug?: boolean;\n /** Override the Swish API URL. Useful for local development (e.g. \"http://localhost:8000\"). */\n apiHost?: string;\n /** @internal Skip before_send registration (already handled by beforeSendHandler). */\n _skipBeforeSend?: boolean;\n}\n\nexport class SwishRecorder {\n private currentSessionId: string | null = null;\n private unsubscribeSessionId: (() => void) | null = null;\n private handleUnload: (() => void) | null = null;\n\n private readonly apiHost: string;\n\n private constructor(private readonly config: SwishRecorderConfig) {\n this.apiHost = config.apiHost ?? API_HOST;\n }\n\n private _log(...args: unknown[]): void {\n if (this.config.debug) console.log(PREFIX, ...args);\n }\n\n private _warn(...args: unknown[]): void {\n if (this.config.debug) console.warn(PREFIX, ...args);\n }\n\n static init(config: SwishRecorderConfig): SwishRecorder {\n const instance = new SwishRecorder(config);\n instance._setup();\n return instance;\n }\n\n /**\n * Create a `before_send` handler to pass directly into PostHog's config.\n * This ensures snapshot interception is active from the very first event,\n * avoiding the race condition where PostHog starts recording from persisted\n * config before `SwishRecorder.init()` can hook in.\n *\n * Usage:\n * const swish = SwishRecorder.beforeSendHandler({ apiHost: '...' })\n * posthog.init(token, { before_send: swish.handler })\n * // later, after posthog is ready:\n * swish.bind(posthog) // starts session lifecycle tracking\n */\n static beforeSendHandler(opts?: {\n apiHost?: string;\n debug?: boolean;\n }): { handler: BeforeSendFn; bind: (posthog: PostHogLike) => SwishRecorder } {\n const apiHost = opts?.apiHost ?? API_HOST;\n const debug = opts?.debug ?? false;\n let token: string | null = null;\n let pendingSnapshots: { props: Record<string, unknown>; ts: number }[] = [];\n\n const log = (...args: unknown[]) => {\n if (debug) console.log(PREFIX, ...args);\n };\n const warn = (...args: unknown[]) => {\n if (debug) console.warn(PREFIX, ...args);\n };\n\n const sendSnapshot = (props: Record<string, unknown>, ts: number) => {\n fetch(`${apiHost}/replays/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-PostHog-Token\": token!,\n },\n body: JSON.stringify({\n session_id: props.$session_id,\n window_id: props.$window_id,\n snapshot_data: props.$snapshot_data,\n distinct_id: props.distinct_id,\n timestamp: ts,\n sdk_version: VERSION,\n }),\n })\n .then((res) => {\n if (debug) {\n if (res.ok) log(`ingest OK — ${res.status}`);\n else warn(`ingest failed — ${res.status} ${res.statusText}`);\n }\n })\n .catch((err) => warn(\"ingest error —\", err));\n };\n\n const handler: BeforeSendFn = (event) => {\n if (event?.event === \"$snapshot\") {\n const props = event.properties;\n const ts = Date.now();\n if (token) {\n sendSnapshot(props, ts);\n } else {\n // Buffer until bind() provides the token\n pendingSnapshots.push({ props, ts });\n log(`buffered snapshot (pre-bind) — ${pendingSnapshots.length} pending`);\n }\n }\n return event;\n };\n\n const bind = (posthog: PostHogLike): SwishRecorder => {\n token = getToken(posthog);\n log(`bind — token=${token.slice(0, 12)}…`);\n\n // Flush any snapshots captured before bind\n if (pendingSnapshots.length > 0) {\n log(`flushing ${pendingSnapshots.length} buffered snapshots`);\n for (const { props, ts } of pendingSnapshots) {\n sendSnapshot(props, ts);\n }\n pendingSnapshots = [];\n }\n\n return SwishRecorder.init({ posthog, apiHost, debug, _skipBeforeSend: true });\n };\n\n return { handler, bind };\n }\n\n private _sendSessionEnd(sessionId: string, reason: string): void {\n this._log(`session end — session=${sessionId} reason=${reason}`);\n navigator.sendBeacon(\n `${this.apiHost}/replays/session-end`,\n new Blob(\n [\n JSON.stringify({\n session_id: sessionId,\n reason,\n token: getToken(this.config.posthog),\n }),\n ],\n { type: \"application/json\" },\n ),\n );\n }\n\n private _ingestSnapshot(\n sessionId: string,\n windowId: string,\n snapshotData: unknown,\n distinctId: string,\n ): void {\n this._log(\n `ingesting snapshot — session=${sessionId} window=${windowId} distinct_id=${distinctId}`,\n );\n fetch(`${this.apiHost}/replays/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-PostHog-Token\": getToken(this.config.posthog),\n },\n body: JSON.stringify({\n session_id: sessionId,\n window_id: windowId,\n snapshot_data: snapshotData,\n distinct_id: distinctId,\n timestamp: Date.now(),\n sdk_version: VERSION,\n }),\n })\n .then((res) => {\n if (this.config.debug) {\n if (res.ok) {\n this._log(`ingest OK — ${res.status}`);\n } else {\n this._warn(`ingest failed — ${res.status} ${res.statusText}`);\n }\n }\n })\n .catch((err) => {\n this._warn(\"ingest error —\", err);\n });\n }\n\n private _setup(): void {\n const { posthog } = this.config;\n let token: string;\n try {\n token = getToken(posthog);\n } catch {\n this._warn(\"no PostHog token found — recording disabled\");\n return;\n }\n\n this._log(`init — token=${token.slice(0, 12)}…`);\n\n // 1. Intercept $snapshot events via before_send (non-destructive append)\n // Skip if already registered via beforeSendHandler()\n if (!this.config._skipBeforeSend) {\n const snapshotHandler: BeforeSendFn = (event) => {\n if (event?.event === \"$snapshot\") {\n const props = event.properties;\n this._ingestSnapshot(\n props.$session_id as string,\n props.$window_id as string,\n props.$snapshot_data,\n props.distinct_id as string,\n );\n } else if (this.config.debug && event) {\n this._log(`before_send passthrough — event=${event.event}`);\n }\n return event;\n };\n\n const existing = getBeforeSend(posthog);\n this._log(\n `before_send — existing handler: ${existing ? (Array.isArray(existing) ? `array(${(existing as unknown[]).length})` : \"function\") : \"none\"}`,\n );\n posthog.set_config({\n before_send: existing\n ? Array.isArray(existing)\n ? [...(existing as BeforeSendFn[]), snapshotHandler]\n : [existing as BeforeSendFn, snapshotHandler]\n : snapshotHandler,\n });\n } else {\n this._log(\"before_send — already registered via beforeSendHandler()\");\n }\n\n // 2. Session rotation signal — fires on idle timeout or max session length\n this.unsubscribeSessionId = posthog.onSessionId(\n (newSessionId, _windowId, changeReason) => {\n this._log(\n `session ID changed — new=${newSessionId} reason=${JSON.stringify(changeReason ?? null)}`,\n );\n if (this.currentSessionId && changeReason) {\n const reason = changeReason.activityTimeout\n ? \"activity_timeout\"\n : changeReason.sessionPastMaximumLength\n ? \"session_length\"\n : null;\n if (reason) this._sendSessionEnd(this.currentSessionId, reason);\n }\n this.currentSessionId = newSessionId;\n },\n );\n\n // 3. Tab close — sendBeacon survives page unload, fetch does not\n this.handleUnload = () => {\n if (this.currentSessionId)\n this._sendSessionEnd(this.currentSessionId, \"tab_close\");\n };\n window.addEventListener(\"beforeunload\", this.handleUnload);\n\n this._log(\"ready\");\n }\n\n destroy(): void {\n this._log(\"destroy\");\n this.unsubscribeSessionId?.();\n if (this.handleUnload) {\n window.removeEventListener(\"beforeunload\", this.handleUnload);\n }\n this.unsubscribeSessionId = null;\n this.handleUnload = null;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -3,21 +3,22 @@ interface PostHogLike {
3
3
  before_send?: unknown;
4
4
  }): void;
5
5
  onSessionId(callback: (sessionId: string, windowId: string | null | undefined, changeReason?: {
6
+ noSessionId?: boolean;
6
7
  activityTimeout?: boolean;
7
8
  sessionPastMaximumLength?: boolean;
8
9
  }) => void): () => void;
9
- readonly config: {
10
+ config?: {
10
11
  token: string;
11
12
  before_send?: unknown;
12
13
  };
13
14
  }
14
- type BeforeSendFn = (event: {
15
- event: string;
16
- properties: Record<string, unknown>;
17
- } | null) => {
18
- event: string;
19
- properties: Record<string, unknown>;
20
- } | null;
15
+ /**
16
+ * Compatible with posthog-js BeforeSendFn / CaptureResult.
17
+ * We use `any` for the public handler type so it's assignable to PostHog's
18
+ * `before_send` without users needing type casts — the actual shapes evolve
19
+ * across posthog-js versions and we only inspect `event` + `properties`.
20
+ */
21
+ type BeforeSendFn = (event: any) => any;
21
22
  interface SwishRecorderConfig {
22
23
  /** PostHog instance from your app — token is read automatically. */
23
24
  posthog: PostHogLike;
package/dist/index.d.ts CHANGED
@@ -3,21 +3,22 @@ interface PostHogLike {
3
3
  before_send?: unknown;
4
4
  }): void;
5
5
  onSessionId(callback: (sessionId: string, windowId: string | null | undefined, changeReason?: {
6
+ noSessionId?: boolean;
6
7
  activityTimeout?: boolean;
7
8
  sessionPastMaximumLength?: boolean;
8
9
  }) => void): () => void;
9
- readonly config: {
10
+ config?: {
10
11
  token: string;
11
12
  before_send?: unknown;
12
13
  };
13
14
  }
14
- type BeforeSendFn = (event: {
15
- event: string;
16
- properties: Record<string, unknown>;
17
- } | null) => {
18
- event: string;
19
- properties: Record<string, unknown>;
20
- } | null;
15
+ /**
16
+ * Compatible with posthog-js BeforeSendFn / CaptureResult.
17
+ * We use `any` for the public handler type so it's assignable to PostHog's
18
+ * `before_send` without users needing type casts — the actual shapes evolve
19
+ * across posthog-js versions and we only inspect `event` + `properties`.
20
+ */
21
+ type BeforeSendFn = (event: any) => any;
21
22
  interface SwishRecorderConfig {
22
23
  /** PostHog instance from your app — token is read automatically. */
23
24
  posthog: PostHogLike;
package/dist/index.js CHANGED
@@ -1,6 +1,17 @@
1
1
  // src/index.ts
2
2
  var API_HOST = "https://api.swish.is";
3
3
  var PREFIX = "[swish-recorder]";
4
+ var VERSION = "0.2.1";
5
+ function getToken(posthog) {
6
+ var _a;
7
+ const token = (_a = posthog.config) == null ? void 0 : _a.token;
8
+ if (!token) throw new Error(`${PREFIX} PostHog instance has no config.token`);
9
+ return token;
10
+ }
11
+ function getBeforeSend(posthog) {
12
+ var _a;
13
+ return (_a = posthog.config) == null ? void 0 : _a.before_send;
14
+ }
4
15
  var SwishRecorder = class _SwishRecorder {
5
16
  constructor(config) {
6
17
  this.config = config;
@@ -57,7 +68,8 @@ var SwishRecorder = class _SwishRecorder {
57
68
  window_id: props.$window_id,
58
69
  snapshot_data: props.$snapshot_data,
59
70
  distinct_id: props.distinct_id,
60
- timestamp: ts
71
+ timestamp: ts,
72
+ sdk_version: VERSION
61
73
  })
62
74
  }).then((res) => {
63
75
  if (debug) {
@@ -80,7 +92,7 @@ var SwishRecorder = class _SwishRecorder {
80
92
  return event;
81
93
  };
82
94
  const bind = (posthog) => {
83
- token = posthog.config.token;
95
+ token = getToken(posthog);
84
96
  log(`bind \u2014 token=${token.slice(0, 12)}\u2026`);
85
97
  if (pendingSnapshots.length > 0) {
86
98
  log(`flushing ${pendingSnapshots.length} buffered snapshots`);
@@ -102,7 +114,7 @@ var SwishRecorder = class _SwishRecorder {
102
114
  JSON.stringify({
103
115
  session_id: sessionId,
104
116
  reason,
105
- token: this.config.posthog.config.token
117
+ token: getToken(this.config.posthog)
106
118
  })
107
119
  ],
108
120
  { type: "application/json" }
@@ -117,14 +129,15 @@ var SwishRecorder = class _SwishRecorder {
117
129
  method: "POST",
118
130
  headers: {
119
131
  "Content-Type": "application/json",
120
- "X-PostHog-Token": this.config.posthog.config.token
132
+ "X-PostHog-Token": getToken(this.config.posthog)
121
133
  },
122
134
  body: JSON.stringify({
123
135
  session_id: sessionId,
124
136
  window_id: windowId,
125
137
  snapshot_data: snapshotData,
126
138
  distinct_id: distinctId,
127
- timestamp: Date.now()
139
+ timestamp: Date.now(),
140
+ sdk_version: VERSION
128
141
  })
129
142
  }).then((res) => {
130
143
  if (this.config.debug) {
@@ -140,12 +153,14 @@ var SwishRecorder = class _SwishRecorder {
140
153
  }
141
154
  _setup() {
142
155
  const { posthog } = this.config;
143
- const token = posthog.config.token;
144
- this._log(`init \u2014 token=${token.slice(0, 12)}\u2026`);
145
- if (!token) {
156
+ let token;
157
+ try {
158
+ token = getToken(posthog);
159
+ } catch (e) {
146
160
  this._warn("no PostHog token found \u2014 recording disabled");
147
161
  return;
148
162
  }
163
+ this._log(`init \u2014 token=${token.slice(0, 12)}\u2026`);
149
164
  if (!this.config._skipBeforeSend) {
150
165
  const snapshotHandler = (event) => {
151
166
  if ((event == null ? void 0 : event.event) === "$snapshot") {
@@ -161,7 +176,7 @@ var SwishRecorder = class _SwishRecorder {
161
176
  }
162
177
  return event;
163
178
  };
164
- const existing = posthog.config.before_send;
179
+ const existing = getBeforeSend(posthog);
165
180
  this._log(
166
181
  `before_send \u2014 existing handler: ${existing ? Array.isArray(existing) ? `array(${existing.length})` : "function" : "none"}`
167
182
  );
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Minimal PostHog interface — no posthog-js dependency needed\ninterface PostHogLike {\n set_config(config: { before_send?: unknown }): void;\n onSessionId(\n callback: (\n sessionId: string,\n windowId: string | null | undefined,\n changeReason?: {\n activityTimeout?: boolean;\n sessionPastMaximumLength?: boolean;\n },\n ) => void,\n ): () => void;\n readonly config: {\n token: string;\n before_send?: unknown;\n };\n}\n\ntype BeforeSendFn = (\n event: { event: string; properties: Record<string, unknown> } | null,\n) => { event: string; properties: Record<string, unknown> } | null;\n\nconst API_HOST = \"https://api.swish.is\";\nconst PREFIX = \"[swish-recorder]\";\n\nexport interface SwishRecorderConfig {\n /** PostHog instance from your app — token is read automatically. */\n posthog: PostHogLike;\n /** Enable console logging for debugging. */\n debug?: boolean;\n /** Override the Swish API URL. Useful for local development (e.g. \"http://localhost:8000\"). */\n apiHost?: string;\n /** @internal Skip before_send registration (already handled by beforeSendHandler). */\n _skipBeforeSend?: boolean;\n}\n\nexport class SwishRecorder {\n private currentSessionId: string | null = null;\n private unsubscribeSessionId: (() => void) | null = null;\n private handleUnload: (() => void) | null = null;\n\n private readonly apiHost: string;\n\n private constructor(private readonly config: SwishRecorderConfig) {\n this.apiHost = config.apiHost ?? API_HOST;\n }\n\n private _log(...args: unknown[]): void {\n if (this.config.debug) console.log(PREFIX, ...args);\n }\n\n private _warn(...args: unknown[]): void {\n if (this.config.debug) console.warn(PREFIX, ...args);\n }\n\n static init(config: SwishRecorderConfig): SwishRecorder {\n const instance = new SwishRecorder(config);\n instance._setup();\n return instance;\n }\n\n /**\n * Create a `before_send` handler to pass directly into PostHog's config.\n * This ensures snapshot interception is active from the very first event,\n * avoiding the race condition where PostHog starts recording from persisted\n * config before `SwishRecorder.init()` can hook in.\n *\n * Usage:\n * const swish = SwishRecorder.beforeSendHandler({ apiHost: '...' })\n * posthog.init(token, { before_send: swish.handler })\n * // later, after posthog is ready:\n * swish.bind(posthog) // starts session lifecycle tracking\n */\n static beforeSendHandler(opts?: {\n apiHost?: string;\n debug?: boolean;\n }): { handler: BeforeSendFn; bind: (posthog: PostHogLike) => SwishRecorder } {\n const apiHost = opts?.apiHost ?? API_HOST;\n const debug = opts?.debug ?? false;\n let token: string | null = null;\n let pendingSnapshots: { props: Record<string, unknown>; ts: number }[] = [];\n\n const log = (...args: unknown[]) => {\n if (debug) console.log(PREFIX, ...args);\n };\n const warn = (...args: unknown[]) => {\n if (debug) console.warn(PREFIX, ...args);\n };\n\n const sendSnapshot = (props: Record<string, unknown>, ts: number) => {\n fetch(`${apiHost}/replays/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-PostHog-Token\": token!,\n },\n body: JSON.stringify({\n session_id: props.$session_id,\n window_id: props.$window_id,\n snapshot_data: props.$snapshot_data,\n distinct_id: props.distinct_id,\n timestamp: ts,\n }),\n })\n .then((res) => {\n if (debug) {\n if (res.ok) log(`ingest OK — ${res.status}`);\n else warn(`ingest failed — ${res.status} ${res.statusText}`);\n }\n })\n .catch((err) => warn(\"ingest error —\", err));\n };\n\n const handler: BeforeSendFn = (event) => {\n if (event?.event === \"$snapshot\") {\n const props = event.properties;\n const ts = Date.now();\n if (token) {\n sendSnapshot(props, ts);\n } else {\n // Buffer until bind() provides the token\n pendingSnapshots.push({ props, ts });\n log(`buffered snapshot (pre-bind) — ${pendingSnapshots.length} pending`);\n }\n }\n return event;\n };\n\n const bind = (posthog: PostHogLike): SwishRecorder => {\n token = posthog.config.token;\n log(`bind — token=${token.slice(0, 12)}…`);\n\n // Flush any snapshots captured before bind\n if (pendingSnapshots.length > 0) {\n log(`flushing ${pendingSnapshots.length} buffered snapshots`);\n for (const { props, ts } of pendingSnapshots) {\n sendSnapshot(props, ts);\n }\n pendingSnapshots = [];\n }\n\n return SwishRecorder.init({ posthog, apiHost, debug, _skipBeforeSend: true });\n };\n\n return { handler, bind };\n }\n\n private _sendSessionEnd(sessionId: string, reason: string): void {\n this._log(`session end — session=${sessionId} reason=${reason}`);\n navigator.sendBeacon(\n `${this.apiHost}/replays/session-end`,\n new Blob(\n [\n JSON.stringify({\n session_id: sessionId,\n reason,\n token: this.config.posthog.config.token,\n }),\n ],\n { type: \"application/json\" },\n ),\n );\n }\n\n private _ingestSnapshot(\n sessionId: string,\n windowId: string,\n snapshotData: unknown,\n distinctId: string,\n ): void {\n this._log(\n `ingesting snapshot — session=${sessionId} window=${windowId} distinct_id=${distinctId}`,\n );\n fetch(`${this.apiHost}/replays/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-PostHog-Token\": this.config.posthog.config.token,\n },\n body: JSON.stringify({\n session_id: sessionId,\n window_id: windowId,\n snapshot_data: snapshotData,\n distinct_id: distinctId,\n timestamp: Date.now(),\n }),\n })\n .then((res) => {\n if (this.config.debug) {\n if (res.ok) {\n this._log(`ingest OK — ${res.status}`);\n } else {\n this._warn(`ingest failed — ${res.status} ${res.statusText}`);\n }\n }\n })\n .catch((err) => {\n this._warn(\"ingest error —\", err);\n });\n }\n\n private _setup(): void {\n const { posthog } = this.config;\n const token = posthog.config.token;\n\n this._log(`init — token=${token.slice(0, 12)}…`);\n\n if (!token) {\n this._warn(\"no PostHog token found — recording disabled\");\n return;\n }\n\n // 1. Intercept $snapshot events via before_send (non-destructive append)\n // Skip if already registered via beforeSendHandler()\n if (!this.config._skipBeforeSend) {\n const snapshotHandler: BeforeSendFn = (event) => {\n if (event?.event === \"$snapshot\") {\n const props = event.properties;\n this._ingestSnapshot(\n props.$session_id as string,\n props.$window_id as string,\n props.$snapshot_data,\n props.distinct_id as string,\n );\n } else if (this.config.debug && event) {\n this._log(`before_send passthrough — event=${event.event}`);\n }\n return event;\n };\n\n const existing = posthog.config.before_send;\n this._log(\n `before_send — existing handler: ${existing ? (Array.isArray(existing) ? `array(${(existing as unknown[]).length})` : \"function\") : \"none\"}`,\n );\n posthog.set_config({\n before_send: existing\n ? Array.isArray(existing)\n ? [...(existing as BeforeSendFn[]), snapshotHandler]\n : [existing as BeforeSendFn, snapshotHandler]\n : snapshotHandler,\n });\n } else {\n this._log(\"before_send — already registered via beforeSendHandler()\");\n }\n\n // 2. Session rotation signal — fires on idle timeout or max session length\n this.unsubscribeSessionId = posthog.onSessionId(\n (newSessionId, _windowId, changeReason) => {\n this._log(\n `session ID changed — new=${newSessionId} reason=${JSON.stringify(changeReason ?? null)}`,\n );\n if (this.currentSessionId && changeReason) {\n const reason = changeReason.activityTimeout\n ? \"activity_timeout\"\n : changeReason.sessionPastMaximumLength\n ? \"session_length\"\n : null;\n if (reason) this._sendSessionEnd(this.currentSessionId, reason);\n }\n this.currentSessionId = newSessionId;\n },\n );\n\n // 3. Tab close — sendBeacon survives page unload, fetch does not\n this.handleUnload = () => {\n if (this.currentSessionId)\n this._sendSessionEnd(this.currentSessionId, \"tab_close\");\n };\n window.addEventListener(\"beforeunload\", this.handleUnload);\n\n this._log(\"ready\");\n }\n\n destroy(): void {\n this._log(\"destroy\");\n this.unsubscribeSessionId?.();\n if (this.handleUnload) {\n window.removeEventListener(\"beforeunload\", this.handleUnload);\n }\n this.unsubscribeSessionId = null;\n this.handleUnload = null;\n }\n}\n"],"mappings":";AAuBA,IAAM,WAAW;AACjB,IAAM,SAAS;AAaR,IAAM,gBAAN,MAAM,eAAc;AAAA,EAOjB,YAA6B,QAA6B;AAA7B;AANrC,SAAQ,mBAAkC;AAC1C,SAAQ,uBAA4C;AACpD,SAAQ,eAAoC;AAxC9C;AA6CI,SAAK,WAAU,YAAO,YAAP,YAAkB;AAAA,EACnC;AAAA,EAEQ,QAAQ,MAAuB;AACrC,QAAI,KAAK,OAAO,MAAO,SAAQ,IAAI,QAAQ,GAAG,IAAI;AAAA,EACpD;AAAA,EAEQ,SAAS,MAAuB;AACtC,QAAI,KAAK,OAAO,MAAO,SAAQ,KAAK,QAAQ,GAAG,IAAI;AAAA,EACrD;AAAA,EAEA,OAAO,KAAK,QAA4C;AACtD,UAAM,WAAW,IAAI,eAAc,MAAM;AACzC,aAAS,OAAO;AAChB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,OAAO,kBAAkB,MAGoD;AA7E/E;AA8EI,UAAM,WAAU,kCAAM,YAAN,YAAiB;AACjC,UAAM,SAAQ,kCAAM,UAAN,YAAe;AAC7B,QAAI,QAAuB;AAC3B,QAAI,mBAAqE,CAAC;AAE1E,UAAM,MAAM,IAAI,SAAoB;AAClC,UAAI,MAAO,SAAQ,IAAI,QAAQ,GAAG,IAAI;AAAA,IACxC;AACA,UAAM,OAAO,IAAI,SAAoB;AACnC,UAAI,MAAO,SAAQ,KAAK,QAAQ,GAAG,IAAI;AAAA,IACzC;AAEA,UAAM,eAAe,CAAC,OAAgC,OAAe;AACnE,YAAM,GAAG,OAAO,mBAAmB;AAAA,QACjC,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,mBAAmB;AAAA,QACrB;AAAA,QACA,MAAM,KAAK,UAAU;AAAA,UACnB,YAAY,MAAM;AAAA,UAClB,WAAW,MAAM;AAAA,UACjB,eAAe,MAAM;AAAA,UACrB,aAAa,MAAM;AAAA,UACnB,WAAW;AAAA,QACb,CAAC;AAAA,MACH,CAAC,EACE,KAAK,CAAC,QAAQ;AACb,YAAI,OAAO;AACT,cAAI,IAAI,GAAI,KAAI,oBAAe,IAAI,MAAM,EAAE;AAAA,cACtC,MAAK,wBAAmB,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,QAC7D;AAAA,MACF,CAAC,EACA,MAAM,CAAC,QAAQ,KAAK,uBAAkB,GAAG,CAAC;AAAA,IAC/C;AAEA,UAAM,UAAwB,CAAC,UAAU;AACvC,WAAI,+BAAO,WAAU,aAAa;AAChC,cAAM,QAAQ,MAAM;AACpB,cAAM,KAAK,KAAK,IAAI;AACpB,YAAI,OAAO;AACT,uBAAa,OAAO,EAAE;AAAA,QACxB,OAAO;AAEL,2BAAiB,KAAK,EAAE,OAAO,GAAG,CAAC;AACnC,cAAI,uCAAkC,iBAAiB,MAAM,UAAU;AAAA,QACzE;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,CAAC,YAAwC;AACpD,cAAQ,QAAQ,OAAO;AACvB,UAAI,qBAAgB,MAAM,MAAM,GAAG,EAAE,CAAC,QAAG;AAGzC,UAAI,iBAAiB,SAAS,GAAG;AAC/B,YAAI,YAAY,iBAAiB,MAAM,qBAAqB;AAC5D,mBAAW,EAAE,OAAO,GAAG,KAAK,kBAAkB;AAC5C,uBAAa,OAAO,EAAE;AAAA,QACxB;AACA,2BAAmB,CAAC;AAAA,MACtB;AAEA,aAAO,eAAc,KAAK,EAAE,SAAS,SAAS,OAAO,iBAAiB,KAAK,CAAC;AAAA,IAC9E;AAEA,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AAAA,EAEQ,gBAAgB,WAAmB,QAAsB;AAC/D,SAAK,KAAK,8BAAyB,SAAS,WAAW,MAAM,EAAE;AAC/D,cAAU;AAAA,MACR,GAAG,KAAK,OAAO;AAAA,MACf,IAAI;AAAA,QACF;AAAA,UACE,KAAK,UAAU;AAAA,YACb,YAAY;AAAA,YACZ;AAAA,YACA,OAAO,KAAK,OAAO,QAAQ,OAAO;AAAA,UACpC,CAAC;AAAA,QACH;AAAA,QACA,EAAE,MAAM,mBAAmB;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,gBACN,WACA,UACA,cACA,YACM;AACN,SAAK;AAAA,MACH,qCAAgC,SAAS,WAAW,QAAQ,gBAAgB,UAAU;AAAA,IACxF;AACA,UAAM,GAAG,KAAK,OAAO,mBAAmB;AAAA,MACtC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,mBAAmB,KAAK,OAAO,QAAQ,OAAO;AAAA,MAChD;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,eAAe;AAAA,QACf,aAAa;AAAA,QACb,WAAW,KAAK,IAAI;AAAA,MACtB,CAAC;AAAA,IACH,CAAC,EACE,KAAK,CAAC,QAAQ;AACb,UAAI,KAAK,OAAO,OAAO;AACrB,YAAI,IAAI,IAAI;AACV,eAAK,KAAK,oBAAe,IAAI,MAAM,EAAE;AAAA,QACvC,OAAO;AACL,eAAK,MAAM,wBAAmB,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,QAC9D;AAAA,MACF;AAAA,IACF,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,WAAK,MAAM,uBAAkB,GAAG;AAAA,IAClC,CAAC;AAAA,EACL;AAAA,EAEQ,SAAe;AACrB,UAAM,EAAE,QAAQ,IAAI,KAAK;AACzB,UAAM,QAAQ,QAAQ,OAAO;AAE7B,SAAK,KAAK,qBAAgB,MAAM,MAAM,GAAG,EAAE,CAAC,QAAG;AAE/C,QAAI,CAAC,OAAO;AACV,WAAK,MAAM,kDAA6C;AACxD;AAAA,IACF;AAIA,QAAI,CAAC,KAAK,OAAO,iBAAiB;AAChC,YAAM,kBAAgC,CAAC,UAAU;AAC/C,aAAI,+BAAO,WAAU,aAAa;AAChC,gBAAM,QAAQ,MAAM;AACpB,eAAK;AAAA,YACH,MAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM;AAAA,UACR;AAAA,QACF,WAAW,KAAK,OAAO,SAAS,OAAO;AACrC,eAAK,KAAK,wCAAmC,MAAM,KAAK,EAAE;AAAA,QAC5D;AACA,eAAO;AAAA,MACT;AAEA,YAAM,WAAW,QAAQ,OAAO;AAChC,WAAK;AAAA,QACH,wCAAmC,WAAY,MAAM,QAAQ,QAAQ,IAAI,SAAU,SAAuB,MAAM,MAAM,aAAc,MAAM;AAAA,MAC5I;AACA,cAAQ,WAAW;AAAA,QACjB,aAAa,WACT,MAAM,QAAQ,QAAQ,IACpB,CAAC,GAAI,UAA6B,eAAe,IACjD,CAAC,UAA0B,eAAe,IAC5C;AAAA,MACN,CAAC;AAAA,IACH,OAAO;AACL,WAAK,KAAK,+DAA0D;AAAA,IACtE;AAGA,SAAK,uBAAuB,QAAQ;AAAA,MAClC,CAAC,cAAc,WAAW,iBAAiB;AACzC,aAAK;AAAA,UACH,iCAA4B,YAAY,WAAW,KAAK,UAAU,sCAAgB,IAAI,CAAC;AAAA,QACzF;AACA,YAAI,KAAK,oBAAoB,cAAc;AACzC,gBAAM,SAAS,aAAa,kBACxB,qBACA,aAAa,2BACX,mBACA;AACN,cAAI,OAAQ,MAAK,gBAAgB,KAAK,kBAAkB,MAAM;AAAA,QAChE;AACA,aAAK,mBAAmB;AAAA,MAC1B;AAAA,IACF;AAGA,SAAK,eAAe,MAAM;AACxB,UAAI,KAAK;AACP,aAAK,gBAAgB,KAAK,kBAAkB,WAAW;AAAA,IAC3D;AACA,WAAO,iBAAiB,gBAAgB,KAAK,YAAY;AAEzD,SAAK,KAAK,OAAO;AAAA,EACnB;AAAA,EAEA,UAAgB;AAlRlB;AAmRI,SAAK,KAAK,SAAS;AACnB,eAAK,yBAAL;AACA,QAAI,KAAK,cAAc;AACrB,aAAO,oBAAoB,gBAAgB,KAAK,YAAY;AAAA,IAC9D;AACA,SAAK,uBAAuB;AAC5B,SAAK,eAAe;AAAA,EACtB;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Minimal PostHog interface — no posthog-js dependency needed.\n//\n// posthog-js types the `loaded` callback parameter as\n// `PostHogInterface = Omit<PostHog, 'config' | 'init'>`, which strips\n// `config` from the type even though the runtime object always has it.\n// We keep `config` optional here so users can pass either the full\n// PostHog instance or the `loaded` callback parameter without type errors.\n// At runtime we assert `config` exists — it always does.\ninterface PostHogLike {\n set_config(config: { before_send?: unknown }): void;\n onSessionId(\n callback: (\n sessionId: string,\n windowId: string | null | undefined,\n changeReason?: {\n noSessionId?: boolean;\n activityTimeout?: boolean;\n sessionPastMaximumLength?: boolean;\n },\n ) => void,\n ): () => void;\n config?: {\n token: string;\n before_send?: unknown;\n };\n}\n\n/**\n * Compatible with posthog-js BeforeSendFn / CaptureResult.\n * We use `any` for the public handler type so it's assignable to PostHog's\n * `before_send` without users needing type casts — the actual shapes evolve\n * across posthog-js versions and we only inspect `event` + `properties`.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype BeforeSendFn = (event: any) => any;\n\nconst API_HOST = \"https://api.swish.is\";\nconst PREFIX = \"[swish-recorder]\";\nconst VERSION = \"0.2.1\";\n\n/** Read posthog.config.token — always exists at runtime even when typed away. */\nfunction getToken(posthog: PostHogLike): string {\n const token = (posthog as { config?: { token?: string } }).config?.token;\n if (!token) throw new Error(`${PREFIX} PostHog instance has no config.token`);\n return token;\n}\n\n/** Read posthog.config.before_send — may be undefined. */\nfunction getBeforeSend(posthog: PostHogLike): unknown {\n return (posthog as { config?: { before_send?: unknown } }).config\n ?.before_send;\n}\n\nexport interface SwishRecorderConfig {\n /** PostHog instance from your app — token is read automatically. */\n posthog: PostHogLike;\n /** Enable console logging for debugging. */\n debug?: boolean;\n /** Override the Swish API URL. Useful for local development (e.g. \"http://localhost:8000\"). */\n apiHost?: string;\n /** @internal Skip before_send registration (already handled by beforeSendHandler). */\n _skipBeforeSend?: boolean;\n}\n\nexport class SwishRecorder {\n private currentSessionId: string | null = null;\n private unsubscribeSessionId: (() => void) | null = null;\n private handleUnload: (() => void) | null = null;\n\n private readonly apiHost: string;\n\n private constructor(private readonly config: SwishRecorderConfig) {\n this.apiHost = config.apiHost ?? API_HOST;\n }\n\n private _log(...args: unknown[]): void {\n if (this.config.debug) console.log(PREFIX, ...args);\n }\n\n private _warn(...args: unknown[]): void {\n if (this.config.debug) console.warn(PREFIX, ...args);\n }\n\n static init(config: SwishRecorderConfig): SwishRecorder {\n const instance = new SwishRecorder(config);\n instance._setup();\n return instance;\n }\n\n /**\n * Create a `before_send` handler to pass directly into PostHog's config.\n * This ensures snapshot interception is active from the very first event,\n * avoiding the race condition where PostHog starts recording from persisted\n * config before `SwishRecorder.init()` can hook in.\n *\n * Usage:\n * const swish = SwishRecorder.beforeSendHandler({ apiHost: '...' })\n * posthog.init(token, { before_send: swish.handler })\n * // later, after posthog is ready:\n * swish.bind(posthog) // starts session lifecycle tracking\n */\n static beforeSendHandler(opts?: {\n apiHost?: string;\n debug?: boolean;\n }): { handler: BeforeSendFn; bind: (posthog: PostHogLike) => SwishRecorder } {\n const apiHost = opts?.apiHost ?? API_HOST;\n const debug = opts?.debug ?? false;\n let token: string | null = null;\n let pendingSnapshots: { props: Record<string, unknown>; ts: number }[] = [];\n\n const log = (...args: unknown[]) => {\n if (debug) console.log(PREFIX, ...args);\n };\n const warn = (...args: unknown[]) => {\n if (debug) console.warn(PREFIX, ...args);\n };\n\n const sendSnapshot = (props: Record<string, unknown>, ts: number) => {\n fetch(`${apiHost}/replays/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-PostHog-Token\": token!,\n },\n body: JSON.stringify({\n session_id: props.$session_id,\n window_id: props.$window_id,\n snapshot_data: props.$snapshot_data,\n distinct_id: props.distinct_id,\n timestamp: ts,\n sdk_version: VERSION,\n }),\n })\n .then((res) => {\n if (debug) {\n if (res.ok) log(`ingest OK — ${res.status}`);\n else warn(`ingest failed — ${res.status} ${res.statusText}`);\n }\n })\n .catch((err) => warn(\"ingest error —\", err));\n };\n\n const handler: BeforeSendFn = (event) => {\n if (event?.event === \"$snapshot\") {\n const props = event.properties;\n const ts = Date.now();\n if (token) {\n sendSnapshot(props, ts);\n } else {\n // Buffer until bind() provides the token\n pendingSnapshots.push({ props, ts });\n log(`buffered snapshot (pre-bind) — ${pendingSnapshots.length} pending`);\n }\n }\n return event;\n };\n\n const bind = (posthog: PostHogLike): SwishRecorder => {\n token = getToken(posthog);\n log(`bind — token=${token.slice(0, 12)}…`);\n\n // Flush any snapshots captured before bind\n if (pendingSnapshots.length > 0) {\n log(`flushing ${pendingSnapshots.length} buffered snapshots`);\n for (const { props, ts } of pendingSnapshots) {\n sendSnapshot(props, ts);\n }\n pendingSnapshots = [];\n }\n\n return SwishRecorder.init({ posthog, apiHost, debug, _skipBeforeSend: true });\n };\n\n return { handler, bind };\n }\n\n private _sendSessionEnd(sessionId: string, reason: string): void {\n this._log(`session end — session=${sessionId} reason=${reason}`);\n navigator.sendBeacon(\n `${this.apiHost}/replays/session-end`,\n new Blob(\n [\n JSON.stringify({\n session_id: sessionId,\n reason,\n token: getToken(this.config.posthog),\n }),\n ],\n { type: \"application/json\" },\n ),\n );\n }\n\n private _ingestSnapshot(\n sessionId: string,\n windowId: string,\n snapshotData: unknown,\n distinctId: string,\n ): void {\n this._log(\n `ingesting snapshot — session=${sessionId} window=${windowId} distinct_id=${distinctId}`,\n );\n fetch(`${this.apiHost}/replays/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-PostHog-Token\": getToken(this.config.posthog),\n },\n body: JSON.stringify({\n session_id: sessionId,\n window_id: windowId,\n snapshot_data: snapshotData,\n distinct_id: distinctId,\n timestamp: Date.now(),\n sdk_version: VERSION,\n }),\n })\n .then((res) => {\n if (this.config.debug) {\n if (res.ok) {\n this._log(`ingest OK — ${res.status}`);\n } else {\n this._warn(`ingest failed — ${res.status} ${res.statusText}`);\n }\n }\n })\n .catch((err) => {\n this._warn(\"ingest error —\", err);\n });\n }\n\n private _setup(): void {\n const { posthog } = this.config;\n let token: string;\n try {\n token = getToken(posthog);\n } catch {\n this._warn(\"no PostHog token found — recording disabled\");\n return;\n }\n\n this._log(`init — token=${token.slice(0, 12)}…`);\n\n // 1. Intercept $snapshot events via before_send (non-destructive append)\n // Skip if already registered via beforeSendHandler()\n if (!this.config._skipBeforeSend) {\n const snapshotHandler: BeforeSendFn = (event) => {\n if (event?.event === \"$snapshot\") {\n const props = event.properties;\n this._ingestSnapshot(\n props.$session_id as string,\n props.$window_id as string,\n props.$snapshot_data,\n props.distinct_id as string,\n );\n } else if (this.config.debug && event) {\n this._log(`before_send passthrough — event=${event.event}`);\n }\n return event;\n };\n\n const existing = getBeforeSend(posthog);\n this._log(\n `before_send — existing handler: ${existing ? (Array.isArray(existing) ? `array(${(existing as unknown[]).length})` : \"function\") : \"none\"}`,\n );\n posthog.set_config({\n before_send: existing\n ? Array.isArray(existing)\n ? [...(existing as BeforeSendFn[]), snapshotHandler]\n : [existing as BeforeSendFn, snapshotHandler]\n : snapshotHandler,\n });\n } else {\n this._log(\"before_send — already registered via beforeSendHandler()\");\n }\n\n // 2. Session rotation signal — fires on idle timeout or max session length\n this.unsubscribeSessionId = posthog.onSessionId(\n (newSessionId, _windowId, changeReason) => {\n this._log(\n `session ID changed — new=${newSessionId} reason=${JSON.stringify(changeReason ?? null)}`,\n );\n if (this.currentSessionId && changeReason) {\n const reason = changeReason.activityTimeout\n ? \"activity_timeout\"\n : changeReason.sessionPastMaximumLength\n ? \"session_length\"\n : null;\n if (reason) this._sendSessionEnd(this.currentSessionId, reason);\n }\n this.currentSessionId = newSessionId;\n },\n );\n\n // 3. Tab close — sendBeacon survives page unload, fetch does not\n this.handleUnload = () => {\n if (this.currentSessionId)\n this._sendSessionEnd(this.currentSessionId, \"tab_close\");\n };\n window.addEventListener(\"beforeunload\", this.handleUnload);\n\n this._log(\"ready\");\n }\n\n destroy(): void {\n this._log(\"destroy\");\n this.unsubscribeSessionId?.();\n if (this.handleUnload) {\n window.removeEventListener(\"beforeunload\", this.handleUnload);\n }\n this.unsubscribeSessionId = null;\n this.handleUnload = null;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swish-recorder",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "First-party session recording SDK for Swish",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",