swish-recorder 0.1.3 → 0.1.5
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 +41 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +41 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -24,12 +24,21 @@ __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;
|
|
30
31
|
this.currentSessionId = null;
|
|
31
32
|
this.unsubscribeSessionId = null;
|
|
32
33
|
this.handleUnload = null;
|
|
34
|
+
var _a;
|
|
35
|
+
this.apiHost = (_a = config.apiHost) != null ? _a : API_HOST;
|
|
36
|
+
}
|
|
37
|
+
_log(...args) {
|
|
38
|
+
if (this.config.debug) console.log(PREFIX, ...args);
|
|
39
|
+
}
|
|
40
|
+
_warn(...args) {
|
|
41
|
+
if (this.config.debug) console.warn(PREFIX, ...args);
|
|
33
42
|
}
|
|
34
43
|
static init(config) {
|
|
35
44
|
const instance = new _SwishRecorder(config);
|
|
@@ -37,8 +46,9 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
37
46
|
return instance;
|
|
38
47
|
}
|
|
39
48
|
_sendSessionEnd(sessionId, reason) {
|
|
49
|
+
this._log(`session end \u2014 session=${sessionId} reason=${reason}`);
|
|
40
50
|
navigator.sendBeacon(
|
|
41
|
-
`${
|
|
51
|
+
`${this.apiHost}/replays/session-end`,
|
|
42
52
|
new Blob(
|
|
43
53
|
[
|
|
44
54
|
JSON.stringify({
|
|
@@ -52,7 +62,10 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
52
62
|
);
|
|
53
63
|
}
|
|
54
64
|
_ingestSnapshot(sessionId, windowId, snapshotData, distinctId) {
|
|
55
|
-
|
|
65
|
+
this._log(
|
|
66
|
+
`ingesting snapshot \u2014 session=${sessionId} window=${windowId} distinct_id=${distinctId}`
|
|
67
|
+
);
|
|
68
|
+
fetch(`${this.apiHost}/replays/ingest`, {
|
|
56
69
|
method: "POST",
|
|
57
70
|
headers: {
|
|
58
71
|
"Content-Type": "application/json",
|
|
@@ -65,11 +78,26 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
65
78
|
distinct_id: distinctId,
|
|
66
79
|
timestamp: Date.now()
|
|
67
80
|
})
|
|
68
|
-
}).
|
|
81
|
+
}).then((res) => {
|
|
82
|
+
if (this.config.debug) {
|
|
83
|
+
if (res.ok) {
|
|
84
|
+
this._log(`ingest OK \u2014 ${res.status}`);
|
|
85
|
+
} else {
|
|
86
|
+
this._warn(`ingest failed \u2014 ${res.status} ${res.statusText}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}).catch((err) => {
|
|
90
|
+
this._warn("ingest error \u2014", err);
|
|
69
91
|
});
|
|
70
92
|
}
|
|
71
93
|
_setup() {
|
|
72
94
|
const { posthog } = this.config;
|
|
95
|
+
const token = posthog.config.token;
|
|
96
|
+
this._log(`init \u2014 token=${token.slice(0, 12)}\u2026`);
|
|
97
|
+
if (!token) {
|
|
98
|
+
this._warn("no PostHog token found \u2014 recording disabled");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
73
101
|
const snapshotHandler = (event) => {
|
|
74
102
|
if ((event == null ? void 0 : event.event) === "$snapshot") {
|
|
75
103
|
const props = event.properties;
|
|
@@ -79,15 +107,23 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
79
107
|
props.$snapshot_data,
|
|
80
108
|
props.distinct_id
|
|
81
109
|
);
|
|
110
|
+
} else if (this.config.debug && event) {
|
|
111
|
+
this._log(`before_send passthrough \u2014 event=${event.event}`);
|
|
82
112
|
}
|
|
83
113
|
return event;
|
|
84
114
|
};
|
|
85
115
|
const existing = posthog.config.before_send;
|
|
116
|
+
this._log(
|
|
117
|
+
`before_send \u2014 existing handler: ${existing ? Array.isArray(existing) ? `array(${existing.length})` : "function" : "none"}`
|
|
118
|
+
);
|
|
86
119
|
posthog.set_config({
|
|
87
120
|
before_send: existing ? Array.isArray(existing) ? [...existing, snapshotHandler] : [existing, snapshotHandler] : snapshotHandler
|
|
88
121
|
});
|
|
89
122
|
this.unsubscribeSessionId = posthog.onSessionId(
|
|
90
123
|
(newSessionId, _windowId, changeReason) => {
|
|
124
|
+
this._log(
|
|
125
|
+
`session ID changed \u2014 new=${newSessionId} reason=${JSON.stringify(changeReason != null ? changeReason : null)}`
|
|
126
|
+
);
|
|
91
127
|
if (this.currentSessionId && changeReason) {
|
|
92
128
|
const reason = changeReason.activityTimeout ? "activity_timeout" : changeReason.sessionPastMaximumLength ? "session_length" : null;
|
|
93
129
|
if (reason) this._sendSessionEnd(this.currentSessionId, reason);
|
|
@@ -100,9 +136,11 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
100
136
|
this._sendSessionEnd(this.currentSessionId, "tab_close");
|
|
101
137
|
};
|
|
102
138
|
window.addEventListener("beforeunload", this.handleUnload);
|
|
139
|
+
this._log("ready");
|
|
103
140
|
}
|
|
104
141
|
destroy() {
|
|
105
142
|
var _a;
|
|
143
|
+
this._log("destroy");
|
|
106
144
|
(_a = this.unsubscribeSessionId) == null ? void 0 : _a.call(this);
|
|
107
145
|
if (this.handleUnload) {
|
|
108
146
|
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 `${
|
|
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}\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 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 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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBA,IAAM,WAAW;AACjB,IAAM,SAAS;AAWR,IAAM,gBAAN,MAAM,eAAc;AAAA,EAOjB,YAA6B,QAA6B;AAA7B;AANrC,SAAQ,mBAAkC;AAC1C,SAAQ,uBAA4C;AACpD,SAAQ,eAAoC;AAtC9C;AA2CI,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,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;AAGA,UAAM,kBAAgC,CAAC,UAAU;AAC/C,WAAI,+BAAO,WAAU,aAAa;AAChC,cAAM,QAAQ,MAAM;AACpB,aAAK;AAAA,UACH,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,QACR;AAAA,MACF,WAAW,KAAK,OAAO,SAAS,OAAO;AACrC,aAAK,KAAK,wCAAmC,MAAM,KAAK,EAAE;AAAA,MAC5D;AACA,aAAO;AAAA,IACT;AAEA,UAAM,WAAW,QAAQ,OAAO;AAChC,SAAK;AAAA,MACH,wCAAmC,WAAY,MAAM,QAAQ,QAAQ,IAAI,SAAU,SAAuB,MAAM,MAAM,aAAc,MAAM;AAAA,IAC5I;AACA,YAAQ,WAAW;AAAA,MACjB,aAAa,WACT,MAAM,QAAQ,QAAQ,IACpB,CAAC,GAAI,UAA6B,eAAe,IACjD,CAAC,UAA0B,eAAe,IAC5C;AAAA,IACN,CAAC;AAGD,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;AArLlB;AAsLI,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":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -14,13 +14,20 @@ 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;
|
|
19
|
+
/** Override the Swish API URL. Useful for local development (e.g. "http://localhost:8000"). */
|
|
20
|
+
apiHost?: string;
|
|
17
21
|
}
|
|
18
22
|
declare class SwishRecorder {
|
|
19
23
|
private readonly config;
|
|
20
24
|
private currentSessionId;
|
|
21
25
|
private unsubscribeSessionId;
|
|
22
26
|
private handleUnload;
|
|
27
|
+
private readonly apiHost;
|
|
23
28
|
private constructor();
|
|
29
|
+
private _log;
|
|
30
|
+
private _warn;
|
|
24
31
|
static init(config: SwishRecorderConfig): SwishRecorder;
|
|
25
32
|
private _sendSessionEnd;
|
|
26
33
|
private _ingestSnapshot;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,13 +14,20 @@ 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;
|
|
19
|
+
/** Override the Swish API URL. Useful for local development (e.g. "http://localhost:8000"). */
|
|
20
|
+
apiHost?: string;
|
|
17
21
|
}
|
|
18
22
|
declare class SwishRecorder {
|
|
19
23
|
private readonly config;
|
|
20
24
|
private currentSessionId;
|
|
21
25
|
private unsubscribeSessionId;
|
|
22
26
|
private handleUnload;
|
|
27
|
+
private readonly apiHost;
|
|
23
28
|
private constructor();
|
|
29
|
+
private _log;
|
|
30
|
+
private _warn;
|
|
24
31
|
static init(config: SwishRecorderConfig): SwishRecorder;
|
|
25
32
|
private _sendSessionEnd;
|
|
26
33
|
private _ingestSnapshot;
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
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;
|
|
6
7
|
this.currentSessionId = null;
|
|
7
8
|
this.unsubscribeSessionId = null;
|
|
8
9
|
this.handleUnload = null;
|
|
10
|
+
var _a;
|
|
11
|
+
this.apiHost = (_a = config.apiHost) != null ? _a : API_HOST;
|
|
12
|
+
}
|
|
13
|
+
_log(...args) {
|
|
14
|
+
if (this.config.debug) console.log(PREFIX, ...args);
|
|
15
|
+
}
|
|
16
|
+
_warn(...args) {
|
|
17
|
+
if (this.config.debug) console.warn(PREFIX, ...args);
|
|
9
18
|
}
|
|
10
19
|
static init(config) {
|
|
11
20
|
const instance = new _SwishRecorder(config);
|
|
@@ -13,8 +22,9 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
13
22
|
return instance;
|
|
14
23
|
}
|
|
15
24
|
_sendSessionEnd(sessionId, reason) {
|
|
25
|
+
this._log(`session end \u2014 session=${sessionId} reason=${reason}`);
|
|
16
26
|
navigator.sendBeacon(
|
|
17
|
-
`${
|
|
27
|
+
`${this.apiHost}/replays/session-end`,
|
|
18
28
|
new Blob(
|
|
19
29
|
[
|
|
20
30
|
JSON.stringify({
|
|
@@ -28,7 +38,10 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
28
38
|
);
|
|
29
39
|
}
|
|
30
40
|
_ingestSnapshot(sessionId, windowId, snapshotData, distinctId) {
|
|
31
|
-
|
|
41
|
+
this._log(
|
|
42
|
+
`ingesting snapshot \u2014 session=${sessionId} window=${windowId} distinct_id=${distinctId}`
|
|
43
|
+
);
|
|
44
|
+
fetch(`${this.apiHost}/replays/ingest`, {
|
|
32
45
|
method: "POST",
|
|
33
46
|
headers: {
|
|
34
47
|
"Content-Type": "application/json",
|
|
@@ -41,11 +54,26 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
41
54
|
distinct_id: distinctId,
|
|
42
55
|
timestamp: Date.now()
|
|
43
56
|
})
|
|
44
|
-
}).
|
|
57
|
+
}).then((res) => {
|
|
58
|
+
if (this.config.debug) {
|
|
59
|
+
if (res.ok) {
|
|
60
|
+
this._log(`ingest OK \u2014 ${res.status}`);
|
|
61
|
+
} else {
|
|
62
|
+
this._warn(`ingest failed \u2014 ${res.status} ${res.statusText}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}).catch((err) => {
|
|
66
|
+
this._warn("ingest error \u2014", err);
|
|
45
67
|
});
|
|
46
68
|
}
|
|
47
69
|
_setup() {
|
|
48
70
|
const { posthog } = this.config;
|
|
71
|
+
const token = posthog.config.token;
|
|
72
|
+
this._log(`init \u2014 token=${token.slice(0, 12)}\u2026`);
|
|
73
|
+
if (!token) {
|
|
74
|
+
this._warn("no PostHog token found \u2014 recording disabled");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
49
77
|
const snapshotHandler = (event) => {
|
|
50
78
|
if ((event == null ? void 0 : event.event) === "$snapshot") {
|
|
51
79
|
const props = event.properties;
|
|
@@ -55,15 +83,23 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
55
83
|
props.$snapshot_data,
|
|
56
84
|
props.distinct_id
|
|
57
85
|
);
|
|
86
|
+
} else if (this.config.debug && event) {
|
|
87
|
+
this._log(`before_send passthrough \u2014 event=${event.event}`);
|
|
58
88
|
}
|
|
59
89
|
return event;
|
|
60
90
|
};
|
|
61
91
|
const existing = posthog.config.before_send;
|
|
92
|
+
this._log(
|
|
93
|
+
`before_send \u2014 existing handler: ${existing ? Array.isArray(existing) ? `array(${existing.length})` : "function" : "none"}`
|
|
94
|
+
);
|
|
62
95
|
posthog.set_config({
|
|
63
96
|
before_send: existing ? Array.isArray(existing) ? [...existing, snapshotHandler] : [existing, snapshotHandler] : snapshotHandler
|
|
64
97
|
});
|
|
65
98
|
this.unsubscribeSessionId = posthog.onSessionId(
|
|
66
99
|
(newSessionId, _windowId, changeReason) => {
|
|
100
|
+
this._log(
|
|
101
|
+
`session ID changed \u2014 new=${newSessionId} reason=${JSON.stringify(changeReason != null ? changeReason : null)}`
|
|
102
|
+
);
|
|
67
103
|
if (this.currentSessionId && changeReason) {
|
|
68
104
|
const reason = changeReason.activityTimeout ? "activity_timeout" : changeReason.sessionPastMaximumLength ? "session_length" : null;
|
|
69
105
|
if (reason) this._sendSessionEnd(this.currentSessionId, reason);
|
|
@@ -76,9 +112,11 @@ var SwishRecorder = class _SwishRecorder {
|
|
|
76
112
|
this._sendSessionEnd(this.currentSessionId, "tab_close");
|
|
77
113
|
};
|
|
78
114
|
window.addEventListener("beforeunload", this.handleUnload);
|
|
115
|
+
this._log("ready");
|
|
79
116
|
}
|
|
80
117
|
destroy() {
|
|
81
118
|
var _a;
|
|
119
|
+
this._log("destroy");
|
|
82
120
|
(_a = this.unsubscribeSessionId) == null ? void 0 : _a.call(this);
|
|
83
121
|
if (this.handleUnload) {
|
|
84
122
|
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 `${
|
|
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}\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 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 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":[]}
|