swish-recorder 0.1.2 → 0.1.4
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/README.md +19 -6
- package/dist/index.cjs +37 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,11 +29,15 @@ npm install swish-recorder
|
|
|
29
29
|
pnpm add swish-recorder
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
`posthog-js` must already be installed in your project. `swish-recorder` has no dependencies of its own.
|
|
33
|
+
|
|
32
34
|
---
|
|
33
35
|
|
|
34
36
|
## Usage
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
### Vanilla JS
|
|
39
|
+
|
|
40
|
+
Call `SwishRecorder.init` right after `posthog.init`. The project token is read automatically from the PostHog instance — no extra credentials needed.
|
|
37
41
|
|
|
38
42
|
```ts
|
|
39
43
|
import posthog from 'posthog-js'
|
|
@@ -49,26 +53,35 @@ SwishRecorder.init({ posthog })
|
|
|
49
53
|
|
|
50
54
|
### React
|
|
51
55
|
|
|
56
|
+
Initialize PostHog before mounting your app, then pass the instance to `PostHogProvider` via `client=`. Call `SwishRecorder.init` in between — before `createRoot`.
|
|
57
|
+
|
|
52
58
|
```tsx
|
|
53
59
|
// src/main.tsx
|
|
60
|
+
import { StrictMode } from 'react'
|
|
61
|
+
import { createRoot } from 'react-dom/client'
|
|
54
62
|
import posthog from 'posthog-js'
|
|
55
63
|
import { PostHogProvider } from 'posthog-js/react'
|
|
56
64
|
import { SwishRecorder } from 'swish-recorder'
|
|
65
|
+
import App from './App'
|
|
57
66
|
|
|
58
|
-
posthog.init(
|
|
59
|
-
api_host:
|
|
67
|
+
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_TOKEN, {
|
|
68
|
+
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
|
|
60
69
|
defaults: '2026-01-30',
|
|
61
70
|
})
|
|
62
71
|
|
|
63
72
|
SwishRecorder.init({ posthog })
|
|
64
73
|
|
|
65
74
|
createRoot(document.getElementById('root')!).render(
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
75
|
+
<StrictMode>
|
|
76
|
+
<PostHogProvider client={posthog}>
|
|
77
|
+
<App />
|
|
78
|
+
</PostHogProvider>
|
|
79
|
+
</StrictMode>
|
|
69
80
|
)
|
|
70
81
|
```
|
|
71
82
|
|
|
83
|
+
> If you use `@posthog/react` instead of `posthog-js/react`, replace the import accordingly — the API is identical.
|
|
84
|
+
|
|
72
85
|
---
|
|
73
86
|
|
|
74
87
|
## API
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(index_exports);
|
|
26
26
|
var API_HOST = "https://api.swish.is";
|
|
27
|
+
var PREFIX = "[swish-recorder]";
|
|
27
28
|
var SwishRecorder = class _SwishRecorder {
|
|
28
29
|
constructor(config) {
|
|
29
30
|
this.config = config;
|
|
@@ -31,12 +32,19 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
31
32
|
this.unsubscribeSessionId = null;
|
|
32
33
|
this.handleUnload = null;
|
|
33
34
|
}
|
|
35
|
+
_log(...args) {
|
|
36
|
+
if (this.config.debug) console.log(PREFIX, ...args);
|
|
37
|
+
}
|
|
38
|
+
_warn(...args) {
|
|
39
|
+
if (this.config.debug) console.warn(PREFIX, ...args);
|
|
40
|
+
}
|
|
34
41
|
static init(config) {
|
|
35
42
|
const instance = new _SwishRecorder(config);
|
|
36
43
|
instance._setup();
|
|
37
44
|
return instance;
|
|
38
45
|
}
|
|
39
46
|
_sendSessionEnd(sessionId, reason) {
|
|
47
|
+
this._log(`session end \u2014 session=${sessionId} reason=${reason}`);
|
|
40
48
|
navigator.sendBeacon(
|
|
41
49
|
`${API_HOST}/replays/session-end`,
|
|
42
50
|
new Blob(
|
|
@@ -52,6 +60,9 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
52
60
|
);
|
|
53
61
|
}
|
|
54
62
|
_ingestSnapshot(sessionId, windowId, snapshotData, distinctId) {
|
|
63
|
+
this._log(
|
|
64
|
+
`ingesting snapshot \u2014 session=${sessionId} window=${windowId} distinct_id=${distinctId}`
|
|
65
|
+
);
|
|
55
66
|
fetch(`${API_HOST}/replays/ingest`, {
|
|
56
67
|
method: "POST",
|
|
57
68
|
headers: {
|
|
@@ -65,11 +76,26 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
65
76
|
distinct_id: distinctId,
|
|
66
77
|
timestamp: Date.now()
|
|
67
78
|
})
|
|
68
|
-
}).
|
|
79
|
+
}).then((res) => {
|
|
80
|
+
if (this.config.debug) {
|
|
81
|
+
if (res.ok) {
|
|
82
|
+
this._log(`ingest OK \u2014 ${res.status}`);
|
|
83
|
+
} else {
|
|
84
|
+
this._warn(`ingest failed \u2014 ${res.status} ${res.statusText}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}).catch((err) => {
|
|
88
|
+
this._warn("ingest error \u2014", err);
|
|
69
89
|
});
|
|
70
90
|
}
|
|
71
91
|
_setup() {
|
|
72
92
|
const { posthog } = this.config;
|
|
93
|
+
const token = posthog.config.token;
|
|
94
|
+
this._log(`init \u2014 token=${token.slice(0, 12)}\u2026`);
|
|
95
|
+
if (!token) {
|
|
96
|
+
this._warn("no PostHog token found \u2014 recording disabled");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
73
99
|
const snapshotHandler = (event) => {
|
|
74
100
|
if ((event == null ? void 0 : event.event) === "$snapshot") {
|
|
75
101
|
const props = event.properties;
|
|
@@ -79,15 +105,23 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
79
105
|
props.$snapshot_data,
|
|
80
106
|
props.distinct_id
|
|
81
107
|
);
|
|
108
|
+
} else if (this.config.debug && event) {
|
|
109
|
+
this._log(`before_send passthrough \u2014 event=${event.event}`);
|
|
82
110
|
}
|
|
83
111
|
return event;
|
|
84
112
|
};
|
|
85
113
|
const existing = posthog.config.before_send;
|
|
114
|
+
this._log(
|
|
115
|
+
`before_send \u2014 existing handler: ${existing ? Array.isArray(existing) ? `array(${existing.length})` : "function" : "none"}`
|
|
116
|
+
);
|
|
86
117
|
posthog.set_config({
|
|
87
118
|
before_send: existing ? Array.isArray(existing) ? [...existing, snapshotHandler] : [existing, snapshotHandler] : snapshotHandler
|
|
88
119
|
});
|
|
89
120
|
this.unsubscribeSessionId = posthog.onSessionId(
|
|
90
121
|
(newSessionId, _windowId, changeReason) => {
|
|
122
|
+
this._log(
|
|
123
|
+
`session ID changed \u2014 new=${newSessionId} reason=${JSON.stringify(changeReason != null ? changeReason : null)}`
|
|
124
|
+
);
|
|
91
125
|
if (this.currentSessionId && changeReason) {
|
|
92
126
|
const reason = changeReason.activityTimeout ? "activity_timeout" : changeReason.sessionPastMaximumLength ? "session_length" : null;
|
|
93
127
|
if (reason) this._sendSessionEnd(this.currentSessionId, reason);
|
|
@@ -100,9 +134,11 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
100
134
|
this._sendSessionEnd(this.currentSessionId, "tab_close");
|
|
101
135
|
};
|
|
102
136
|
window.addEventListener("beforeunload", this.handleUnload);
|
|
137
|
+
this._log("ready");
|
|
103
138
|
}
|
|
104
139
|
destroy() {
|
|
105
140
|
var _a;
|
|
141
|
+
this._log("destroy");
|
|
106
142
|
(_a = this.unsubscribeSessionId) == null ? void 0 : _a.call(this);
|
|
107
143
|
if (this.handleUnload) {
|
|
108
144
|
window.removeEventListener("beforeunload", this.handleUnload);
|
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\";\n\nexport interface SwishRecorderConfig {\n /** PostHog instance from your app — token is read automatically. */\n posthog: PostHogLike;\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 constructor(private readonly config: SwishRecorderConfig) {}\n\n static init(config: SwishRecorderConfig): SwishRecorder {\n const instance = new SwishRecorder(config);\n instance._setup();\n return instance;\n }\n\n private _sendSessionEnd(sessionId: string, reason: string): void {\n navigator.sendBeacon(\n `${API_HOST}/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 fetch(`${API_HOST}/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 }).
|
|
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}\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 constructor(private readonly config: SwishRecorderConfig) {}\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 private _sendSessionEnd(sessionId: string, reason: string): void {\n this._log(`session end — session=${sessionId} reason=${reason}`);\n navigator.sendBeacon(\n `${API_HOST}/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(`${API_HOST}/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 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\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
|
@@ -14,6 +14,8 @@ interface PostHogLike {
|
|
|
14
14
|
interface SwishRecorderConfig {
|
|
15
15
|
/** PostHog instance from your app — token is read automatically. */
|
|
16
16
|
posthog: PostHogLike;
|
|
17
|
+
/** Enable console logging for debugging. */
|
|
18
|
+
debug?: boolean;
|
|
17
19
|
}
|
|
18
20
|
declare class SwishRecorder {
|
|
19
21
|
private readonly config;
|
|
@@ -21,6 +23,8 @@ declare class SwishRecorder {
|
|
|
21
23
|
private unsubscribeSessionId;
|
|
22
24
|
private handleUnload;
|
|
23
25
|
private constructor();
|
|
26
|
+
private _log;
|
|
27
|
+
private _warn;
|
|
24
28
|
static init(config: SwishRecorderConfig): SwishRecorder;
|
|
25
29
|
private _sendSessionEnd;
|
|
26
30
|
private _ingestSnapshot;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ interface PostHogLike {
|
|
|
14
14
|
interface SwishRecorderConfig {
|
|
15
15
|
/** PostHog instance from your app — token is read automatically. */
|
|
16
16
|
posthog: PostHogLike;
|
|
17
|
+
/** Enable console logging for debugging. */
|
|
18
|
+
debug?: boolean;
|
|
17
19
|
}
|
|
18
20
|
declare class SwishRecorder {
|
|
19
21
|
private readonly config;
|
|
@@ -21,6 +23,8 @@ declare class SwishRecorder {
|
|
|
21
23
|
private unsubscribeSessionId;
|
|
22
24
|
private handleUnload;
|
|
23
25
|
private constructor();
|
|
26
|
+
private _log;
|
|
27
|
+
private _warn;
|
|
24
28
|
static init(config: SwishRecorderConfig): SwishRecorder;
|
|
25
29
|
private _sendSessionEnd;
|
|
26
30
|
private _ingestSnapshot;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
var API_HOST = "https://api.swish.is";
|
|
3
|
+
var PREFIX = "[swish-recorder]";
|
|
3
4
|
var SwishRecorder = class _SwishRecorder {
|
|
4
5
|
constructor(config) {
|
|
5
6
|
this.config = config;
|
|
@@ -7,12 +8,19 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
7
8
|
this.unsubscribeSessionId = null;
|
|
8
9
|
this.handleUnload = null;
|
|
9
10
|
}
|
|
11
|
+
_log(...args) {
|
|
12
|
+
if (this.config.debug) console.log(PREFIX, ...args);
|
|
13
|
+
}
|
|
14
|
+
_warn(...args) {
|
|
15
|
+
if (this.config.debug) console.warn(PREFIX, ...args);
|
|
16
|
+
}
|
|
10
17
|
static init(config) {
|
|
11
18
|
const instance = new _SwishRecorder(config);
|
|
12
19
|
instance._setup();
|
|
13
20
|
return instance;
|
|
14
21
|
}
|
|
15
22
|
_sendSessionEnd(sessionId, reason) {
|
|
23
|
+
this._log(`session end \u2014 session=${sessionId} reason=${reason}`);
|
|
16
24
|
navigator.sendBeacon(
|
|
17
25
|
`${API_HOST}/replays/session-end`,
|
|
18
26
|
new Blob(
|
|
@@ -28,6 +36,9 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
28
36
|
);
|
|
29
37
|
}
|
|
30
38
|
_ingestSnapshot(sessionId, windowId, snapshotData, distinctId) {
|
|
39
|
+
this._log(
|
|
40
|
+
`ingesting snapshot \u2014 session=${sessionId} window=${windowId} distinct_id=${distinctId}`
|
|
41
|
+
);
|
|
31
42
|
fetch(`${API_HOST}/replays/ingest`, {
|
|
32
43
|
method: "POST",
|
|
33
44
|
headers: {
|
|
@@ -41,11 +52,26 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
41
52
|
distinct_id: distinctId,
|
|
42
53
|
timestamp: Date.now()
|
|
43
54
|
})
|
|
44
|
-
}).
|
|
55
|
+
}).then((res) => {
|
|
56
|
+
if (this.config.debug) {
|
|
57
|
+
if (res.ok) {
|
|
58
|
+
this._log(`ingest OK \u2014 ${res.status}`);
|
|
59
|
+
} else {
|
|
60
|
+
this._warn(`ingest failed \u2014 ${res.status} ${res.statusText}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}).catch((err) => {
|
|
64
|
+
this._warn("ingest error \u2014", err);
|
|
45
65
|
});
|
|
46
66
|
}
|
|
47
67
|
_setup() {
|
|
48
68
|
const { posthog } = this.config;
|
|
69
|
+
const token = posthog.config.token;
|
|
70
|
+
this._log(`init \u2014 token=${token.slice(0, 12)}\u2026`);
|
|
71
|
+
if (!token) {
|
|
72
|
+
this._warn("no PostHog token found \u2014 recording disabled");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
49
75
|
const snapshotHandler = (event) => {
|
|
50
76
|
if ((event == null ? void 0 : event.event) === "$snapshot") {
|
|
51
77
|
const props = event.properties;
|
|
@@ -55,15 +81,23 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
55
81
|
props.$snapshot_data,
|
|
56
82
|
props.distinct_id
|
|
57
83
|
);
|
|
84
|
+
} else if (this.config.debug && event) {
|
|
85
|
+
this._log(`before_send passthrough \u2014 event=${event.event}`);
|
|
58
86
|
}
|
|
59
87
|
return event;
|
|
60
88
|
};
|
|
61
89
|
const existing = posthog.config.before_send;
|
|
90
|
+
this._log(
|
|
91
|
+
`before_send \u2014 existing handler: ${existing ? Array.isArray(existing) ? `array(${existing.length})` : "function" : "none"}`
|
|
92
|
+
);
|
|
62
93
|
posthog.set_config({
|
|
63
94
|
before_send: existing ? Array.isArray(existing) ? [...existing, snapshotHandler] : [existing, snapshotHandler] : snapshotHandler
|
|
64
95
|
});
|
|
65
96
|
this.unsubscribeSessionId = posthog.onSessionId(
|
|
66
97
|
(newSessionId, _windowId, changeReason) => {
|
|
98
|
+
this._log(
|
|
99
|
+
`session ID changed \u2014 new=${newSessionId} reason=${JSON.stringify(changeReason != null ? changeReason : null)}`
|
|
100
|
+
);
|
|
67
101
|
if (this.currentSessionId && changeReason) {
|
|
68
102
|
const reason = changeReason.activityTimeout ? "activity_timeout" : changeReason.sessionPastMaximumLength ? "session_length" : null;
|
|
69
103
|
if (reason) this._sendSessionEnd(this.currentSessionId, reason);
|
|
@@ -76,9 +110,11 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
76
110
|
this._sendSessionEnd(this.currentSessionId, "tab_close");
|
|
77
111
|
};
|
|
78
112
|
window.addEventListener("beforeunload", this.handleUnload);
|
|
113
|
+
this._log("ready");
|
|
79
114
|
}
|
|
80
115
|
destroy() {
|
|
81
116
|
var _a;
|
|
117
|
+
this._log("destroy");
|
|
82
118
|
(_a = this.unsubscribeSessionId) == null ? void 0 : _a.call(this);
|
|
83
119
|
if (this.handleUnload) {
|
|
84
120
|
window.removeEventListener("beforeunload", this.handleUnload);
|
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\";\n\nexport interface SwishRecorderConfig {\n /** PostHog instance from your app — token is read automatically. */\n posthog: PostHogLike;\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 constructor(private readonly config: SwishRecorderConfig) {}\n\n static init(config: SwishRecorderConfig): SwishRecorder {\n const instance = new SwishRecorder(config);\n instance._setup();\n return instance;\n }\n\n private _sendSessionEnd(sessionId: string, reason: string): void {\n navigator.sendBeacon(\n `${API_HOST}/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 fetch(`${API_HOST}/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 }).
|
|
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}\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 constructor(private readonly config: SwishRecorderConfig) {}\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 private _sendSessionEnd(sessionId: string, reason: string): void {\n this._log(`session end — session=${sessionId} reason=${reason}`);\n navigator.sendBeacon(\n `${API_HOST}/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(`${API_HOST}/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 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\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":[]}
|