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 +24 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +24 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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
|
);
|
package/dist/index.cjs.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":"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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
|
-
|
|
10
|
+
config?: {
|
|
10
11
|
token: string;
|
|
11
12
|
before_send?: unknown;
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
10
|
+
config?: {
|
|
10
11
|
token: string;
|
|
11
12
|
before_send?: unknown;
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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":"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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":";AAoCA,IAAM,WAAW;AACjB,IAAM,SAAS;AACf,IAAM,UAAU;AAGhB,SAAS,SAAS,SAA8B;AAzChD;AA0CE,QAAM,SAAS,aAA4C,WAA5C,mBAAoD;AACnE,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,GAAG,MAAM,uCAAuC;AAC5E,SAAO;AACT;AAGA,SAAS,cAAc,SAA+B;AAhDtD;AAiDE,UAAQ,aAAmD,WAAnD,mBACJ;AACN;AAaO,IAAM,gBAAN,MAAM,eAAc;AAAA,EAOjB,YAA6B,QAA6B;AAA7B;AANrC,SAAQ,mBAAkC;AAC1C,SAAQ,uBAA4C;AACpD,SAAQ,eAAoC;AAnE9C;AAwEI,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;AAxG/E;AAyGI,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,UACX,aAAa;AAAA,QACf,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,SAAS,OAAO;AACxB,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,SAAS,KAAK,OAAO,OAAO;AAAA,UACrC,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,SAAS,KAAK,OAAO,OAAO;AAAA,MACjD;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,eAAe;AAAA,QACf,aAAa;AAAA,QACb,WAAW,KAAK,IAAI;AAAA,QACpB,aAAa;AAAA,MACf,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,QAAI;AACJ,QAAI;AACF,cAAQ,SAAS,OAAO;AAAA,IAC1B,SAAQ;AACN,WAAK,MAAM,kDAA6C;AACxD;AAAA,IACF;AAEA,SAAK,KAAK,qBAAgB,MAAM,MAAM,GAAG,EAAE,CAAC,QAAG;AAI/C,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,cAAc,OAAO;AACtC,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;AAhTlB;AAiTI,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":[]}
|