@interfere/react 8.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/internal/client.d.mts +5 -3
- package/dist/internal/client.d.mts.map +1 -1
- package/dist/internal/client.mjs +11 -7
- package/dist/internal/client.mjs.map +1 -1
- package/dist/internal/config.d.mts.map +1 -1
- package/dist/internal/config.mjs +3 -2
- package/dist/internal/config.mjs.map +1 -1
- package/dist/internal/plugin-runtime.d.mts +4 -2
- package/dist/internal/plugin-runtime.d.mts.map +1 -1
- package/dist/internal/plugin-runtime.mjs +13 -2
- package/dist/internal/plugin-runtime.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/tracking/api.d.mts.map +1 -1
- package/dist/tracking/api.mjs +19 -4
- package/dist/tracking/api.mjs.map +1 -1
- package/dist/tracking/geo.d.mts +5 -0
- package/dist/tracking/geo.d.mts.map +1 -0
- package/dist/tracking/geo.mjs +44 -0
- package/dist/tracking/geo.mjs.map +1 -0
- package/dist/transport/queue.d.mts +3 -0
- package/dist/transport/queue.d.mts.map +1 -1
- package/dist/transport/queue.mjs +3 -1
- package/dist/transport/queue.mjs.map +1 -1
- package/package.json +15 -18
package/README.md
CHANGED
|
@@ -19,16 +19,23 @@
|
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
22
|
-
<a href="https://interfere.com/docs">Documentation</a>
|
|
22
|
+
<a href="https://support.interfere.com/docs">Documentation</a>
|
|
23
23
|
·
|
|
24
|
-
<a href="https://
|
|
24
|
+
<a href="https://support.interfere.com/roadmap">Feature Requests</a>
|
|
25
|
+
·
|
|
26
|
+
<a href="https://support.interfere.com/requests">Report a Bug</a>
|
|
25
27
|
·
|
|
26
28
|
<a href="mailto:support@interfere.com">Get Help</a>
|
|
27
29
|
</p>
|
|
28
30
|
|
|
29
31
|
---
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
## Framework Guides
|
|
34
|
+
|
|
35
|
+
| Framework | Package | Guide |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Next.js | [`@interfere/next`](https://www.npmjs.com/package/@interfere/next) | [docs/next.md](docs/next.md) |
|
|
38
|
+
| Vite | [`@interfere/vite`](https://www.npmjs.com/package/@interfere/vite) + `@interfere/react` | [docs/vite.md](docs/vite.md) |
|
|
32
39
|
|
|
33
40
|
## Installation
|
|
34
41
|
|
|
@@ -75,6 +82,7 @@ function MyComponent() {
|
|
|
75
82
|
identifier: "usr_123",
|
|
76
83
|
email: "jane@example.com",
|
|
77
84
|
name: "Jane",
|
|
85
|
+
source: { type: "clerk", name: "Clerk" },
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
const sessionId = session.getId();
|
|
@@ -25,19 +25,21 @@ declare class Client {
|
|
|
25
25
|
private fetchRemoteConfig;
|
|
26
26
|
capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void;
|
|
27
27
|
flush(): void;
|
|
28
|
-
dispose(): void
|
|
28
|
+
dispose(): Promise<void>;
|
|
29
29
|
getConsent(): ConsentState | null;
|
|
30
30
|
setConsent(value?: ConsentState): void;
|
|
31
31
|
resetConsent(): void;
|
|
32
32
|
}
|
|
33
33
|
declare function getClient(): Client;
|
|
34
34
|
declare function init(opts?: ClientOptions): void;
|
|
35
|
-
declare function close(): void
|
|
35
|
+
declare function close(): Promise<void>;
|
|
36
36
|
declare const consent: {
|
|
37
37
|
get(): ConsentState | null;
|
|
38
38
|
set(value?: ConsentState): void;
|
|
39
39
|
};
|
|
40
40
|
declare function syncConsent(consentState: ConsentState | undefined): void;
|
|
41
41
|
declare function flush(): void;
|
|
42
|
+
/** @internal Test-only. Resets the module state so init() can be called again. */
|
|
43
|
+
declare function _reset(): void;
|
|
42
44
|
//#endregion
|
|
43
|
-
export { ClientOptions, close, consent, flush, getClient, init, syncConsent };
|
|
45
|
+
export { ClientOptions, _reset, close, consent, flush, getClient, init, syncConsent };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.mts","names":[],"sources":["../../src/internal/client.ts"],"mappings":";;;;;;UAkBiB,aAAA;EACf,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,YAAA;EACrB,OAAA,GAAU,YAAA;EAFkB;;;;;;;EAU5B,OAAA;EACA,OAAA,GAAU,eAAA;AAAA;AAAA,cAGN,MAAA;EAAA,iBACa,QAAA;EAAA,iBACA,KAAA;EAAA,iBACA,OAAA;cAEL,IAAA,EAAM,aAAA,EAAe,OAAA,UAAiB,SAAA;EAAA,QAoC1C,iBAAA;EA0BR,OAAA,WAAkB,SAAA,CAAA,CAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,eAAA,CAAgB,CAAA;EAS/D,KAAA,CAAA;
|
|
1
|
+
{"version":3,"file":"client.d.mts","names":[],"sources":["../../src/internal/client.ts"],"mappings":";;;;;;UAkBiB,aAAA;EACf,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,YAAA;EACrB,OAAA,GAAU,YAAA;EAFkB;;;;;;;EAU5B,OAAA;EACA,OAAA,GAAU,eAAA;AAAA;AAAA,cAGN,MAAA;EAAA,iBACa,QAAA;EAAA,iBACA,KAAA;EAAA,iBACA,OAAA;cAEL,IAAA,EAAM,aAAA,EAAe,OAAA,UAAiB,SAAA;EAAA,QAoC1C,iBAAA;EA0BR,OAAA,WAAkB,SAAA,CAAA,CAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,eAAA,CAAgB,CAAA;EAS/D,KAAA,CAAA;EAIM,OAAA,CAAA,GAAW,OAAA;EAMjB,UAAA,CAAA,GAAc,YAAA;EAId,UAAA,CAAW,KAAA,GAAQ,YAAA;EAInB,YAAA,CAAA;AAAA;AAAA,iBAOc,SAAA,CAAA,GAAa,MAAA;AAAA,iBA6Bb,IAAA,CAAK,IAAA,GAAM,aAAA;AAAA,iBA+BL,KAAA,CAAA,GAAS,OAAA;AAAA,cASlB,OAAA;SACJ,YAAA;cAIK,YAAA;AAAA;AAAA,iBAKE,WAAA,CAAY,YAAA,EAAc,YAAA;AAAA,iBAa1B,KAAA,CAAA;;iBAKA,MAAA,CAAA"}
|
package/dist/internal/client.mjs
CHANGED
|
@@ -63,8 +63,8 @@ var Client = class {
|
|
|
63
63
|
flush() {
|
|
64
64
|
this.queue.flush();
|
|
65
65
|
}
|
|
66
|
-
dispose() {
|
|
67
|
-
this.runtime.dispose();
|
|
66
|
+
async dispose() {
|
|
67
|
+
await this.runtime.dispose();
|
|
68
68
|
teardown();
|
|
69
69
|
this.queue.dispose();
|
|
70
70
|
}
|
|
@@ -88,7 +88,7 @@ function isEnabledByEnvironment() {
|
|
|
88
88
|
if (typeof process === "undefined" || !process.env) return true;
|
|
89
89
|
if (process.env["NODE_ENV"] === "production") return true;
|
|
90
90
|
if (process.env["NODE_ENV"] === void 0) return true;
|
|
91
|
-
return !!process.env["
|
|
91
|
+
return !!(globalThis["__INTERFERE_FORCE_ENABLE__"] || process.env["INTERFERE_FORCE_ENABLE"]);
|
|
92
92
|
} catch {
|
|
93
93
|
return true;
|
|
94
94
|
}
|
|
@@ -96,7 +96,7 @@ function isEnabledByEnvironment() {
|
|
|
96
96
|
function init(opts = {}) {
|
|
97
97
|
if (instance) return;
|
|
98
98
|
if (!(opts.enabled ?? isEnabledByEnvironment())) {
|
|
99
|
-
log.info("Disabled in non-production. Pass enabled: true to init() or set
|
|
99
|
+
log.info("Disabled in non-production. Pass enabled: true to init() or set INTERFERE_FORCE_ENABLE=1.");
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
const buildId = globalThis["__INTERFERE_BUILD_ID__"];
|
|
@@ -107,9 +107,9 @@ function init(opts = {}) {
|
|
|
107
107
|
}
|
|
108
108
|
instance = new Client(opts, buildId, releaseId ?? null);
|
|
109
109
|
}
|
|
110
|
-
function close() {
|
|
110
|
+
async function close() {
|
|
111
111
|
if (!instance) return;
|
|
112
|
-
instance.dispose();
|
|
112
|
+
await instance.dispose();
|
|
113
113
|
instance = null;
|
|
114
114
|
}
|
|
115
115
|
const consent = {
|
|
@@ -131,5 +131,9 @@ function syncConsent(consentState) {
|
|
|
131
131
|
function flush() {
|
|
132
132
|
instance?.flush();
|
|
133
133
|
}
|
|
134
|
+
/** @internal Test-only. Resets the module state so init() can be called again. */
|
|
135
|
+
function _reset() {
|
|
136
|
+
instance = null;
|
|
137
|
+
}
|
|
134
138
|
//#endregion
|
|
135
|
-
export { close, consent, flush, getClient, init, syncConsent };
|
|
139
|
+
export { _reset, close, consent, flush, getClient, init, syncConsent };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":[],"sources":["../../src/internal/client.ts"],"sourcesContent":["import type { EnvelopePayload, EventType } from \"@interfere/types/sdk/envelope\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\nimport type { RemoteConfig } from \"@interfere/types/sdk/remote-config\";\nimport { inferRuntime, normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nimport type { PluginOverrides } from \"../plugins/lib/loader.js\";\nimport { bootstrap, session, teardown } from \"../tracking/api.js\";\nimport { buildHeaders, HttpTransport } from \"../transport/http.js\";\nimport { BatchQueue, type QueueOptions } from \"../transport/queue.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { resolveTargets } from \"./config.js\";\nimport { collectContext } from \"./context.js\";\nimport { buildEnvelope, type EnvelopeMetadata } from \"./envelope.js\";\nimport { PluginRuntime } from \"./plugin-runtime.js\";\nimport { registerServiceWorker } from \"./sw.js\";\n\nconst log = createLogger(\"client\");\n\nexport interface ClientOptions {\n batch?: Omit<Partial<QueueOptions>, \"transport\">;\n consent?: ConsentState;\n /**\n * Override the automatic dev-mode guard. When `undefined`, the SDK\n * auto-detects: it disables itself if `process.env[\"NODE_ENV\"]` is not\n * `\"production\"` (Node / webpack / Next.js). In environments where\n * `process` does not exist (Vite, CRA, plain browser) the SDK\n * defaults to **enabled** — pass `false` to disable explicitly.\n */\n enabled?: boolean;\n plugins?: PluginOverrides;\n}\n\nclass Client {\n private readonly metadata: EnvelopeMetadata;\n private readonly queue: BatchQueue;\n private readonly runtime: PluginRuntime;\n\n constructor(opts: ClientOptions, buildId: string, releaseId: string | null) {\n const targets = resolveTargets();\n bootstrap(targets.session);\n\n log.info(\"target: %s\", targets.ingest.url);\n\n this.metadata = {\n context: collectContext(),\n environment: normalizeEnv(\n typeof process === \"undefined\" ? undefined : process.env[\"NODE_ENV\"]\n ),\n runtime: inferRuntime(),\n buildId,\n releaseId,\n };\n\n registerServiceWorker();\n\n const transport = new HttpTransport(targets.ingest);\n this.queue = new BatchQueue({ transport, ...opts.batch });\n this.queue.start();\n\n this.runtime = new PluginRuntime(\n {\n capture: (type, payload) => this.capture(type, payload),\n getSessionId: () => session.getId() ?? \"\",\n },\n opts.plugins,\n opts.consent\n );\n\n this.runtime.start();\n\n this.fetchRemoteConfig(targets.config);\n }\n\n private fetchRemoteConfig(configTarget: {\n url: string;\n headers: Headers;\n }): void {\n fetch(configTarget.url, {\n method: \"GET\",\n headers: buildHeaders(configTarget.headers),\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n return;\n }\n return res.json() as Promise<RemoteConfig>;\n })\n .then((config) => {\n if (config?.plugins) {\n this.runtime.applyRemoteConfig(config.plugins);\n log.debug(\"applied remote config\");\n }\n })\n .catch(() => {\n log.warn(\"remote config fetch failed, using local defaults\");\n });\n }\n\n capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void {\n const sessionId = session.getId();\n if (!(sessionId && this.runtime.canCapture(type))) {\n return;\n }\n\n this.queue.enqueue(buildEnvelope(type, payload, sessionId, this.metadata));\n }\n\n flush(): void {\n this.queue.flush();\n }\n\n dispose(): void {\n this.runtime.dispose();\n teardown();\n this.queue.dispose();\n }\n\n getConsent(): ConsentState | null {\n return this.runtime.getConsent();\n }\n\n setConsent(value?: ConsentState): void {\n this.runtime.setConsent(value);\n }\n\n resetConsent(): void {\n this.runtime.resetConsent();\n }\n}\n\nlet instance: Client | null = null;\n\nexport function getClient(): Client {\n if (!instance) {\n throw new Error(\n \"Interfere SDK not initialized. Call init() from your instrumentation-client entrypoint.\"\n );\n }\n return instance;\n}\n\nfunction isEnabledByEnvironment(): boolean {\n try {\n if (typeof process === \"undefined\" || !process.env) {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === \"production\") {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === undefined) {\n return true;\n }\n return !!process.env[\"
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../../src/internal/client.ts"],"sourcesContent":["import type { EnvelopePayload, EventType } from \"@interfere/types/sdk/envelope\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\nimport type { RemoteConfig } from \"@interfere/types/sdk/remote-config\";\nimport { inferRuntime, normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nimport type { PluginOverrides } from \"../plugins/lib/loader.js\";\nimport { bootstrap, session, teardown } from \"../tracking/api.js\";\nimport { buildHeaders, HttpTransport } from \"../transport/http.js\";\nimport { BatchQueue, type QueueOptions } from \"../transport/queue.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { resolveTargets } from \"./config.js\";\nimport { collectContext } from \"./context.js\";\nimport { buildEnvelope, type EnvelopeMetadata } from \"./envelope.js\";\nimport { PluginRuntime } from \"./plugin-runtime.js\";\nimport { registerServiceWorker } from \"./sw.js\";\n\nconst log = createLogger(\"client\");\n\nexport interface ClientOptions {\n batch?: Omit<Partial<QueueOptions>, \"transport\">;\n consent?: ConsentState;\n /**\n * Override the automatic dev-mode guard. When `undefined`, the SDK\n * auto-detects: it disables itself if `process.env[\"NODE_ENV\"]` is not\n * `\"production\"` (Node / webpack / Next.js). In environments where\n * `process` does not exist (Vite, CRA, plain browser) the SDK\n * defaults to **enabled** — pass `false` to disable explicitly.\n */\n enabled?: boolean;\n plugins?: PluginOverrides;\n}\n\nclass Client {\n private readonly metadata: EnvelopeMetadata;\n private readonly queue: BatchQueue;\n private readonly runtime: PluginRuntime;\n\n constructor(opts: ClientOptions, buildId: string, releaseId: string | null) {\n const targets = resolveTargets();\n bootstrap(targets.session);\n\n log.info(\"target: %s\", targets.ingest.url);\n\n this.metadata = {\n context: collectContext(),\n environment: normalizeEnv(\n typeof process === \"undefined\" ? undefined : process.env[\"NODE_ENV\"]\n ),\n runtime: inferRuntime(),\n buildId,\n releaseId,\n };\n\n registerServiceWorker();\n\n const transport = new HttpTransport(targets.ingest);\n this.queue = new BatchQueue({ transport, ...opts.batch });\n this.queue.start();\n\n this.runtime = new PluginRuntime(\n {\n capture: (type, payload) => this.capture(type, payload),\n getSessionId: () => session.getId() ?? \"\",\n },\n opts.plugins,\n opts.consent\n );\n\n this.runtime.start();\n\n this.fetchRemoteConfig(targets.config);\n }\n\n private fetchRemoteConfig(configTarget: {\n url: string;\n headers: Headers;\n }): void {\n fetch(configTarget.url, {\n method: \"GET\",\n headers: buildHeaders(configTarget.headers),\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n return;\n }\n return res.json() as Promise<RemoteConfig>;\n })\n .then((config) => {\n if (config?.plugins) {\n this.runtime.applyRemoteConfig(config.plugins);\n log.debug(\"applied remote config\");\n }\n })\n .catch(() => {\n log.warn(\"remote config fetch failed, using local defaults\");\n });\n }\n\n capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void {\n const sessionId = session.getId();\n if (!(sessionId && this.runtime.canCapture(type))) {\n return;\n }\n\n this.queue.enqueue(buildEnvelope(type, payload, sessionId, this.metadata));\n }\n\n flush(): void {\n this.queue.flush();\n }\n\n async dispose(): Promise<void> {\n await this.runtime.dispose();\n teardown();\n this.queue.dispose();\n }\n\n getConsent(): ConsentState | null {\n return this.runtime.getConsent();\n }\n\n setConsent(value?: ConsentState): void {\n this.runtime.setConsent(value);\n }\n\n resetConsent(): void {\n this.runtime.resetConsent();\n }\n}\n\nlet instance: Client | null = null;\n\nexport function getClient(): Client {\n if (!instance) {\n throw new Error(\n \"Interfere SDK not initialized. Call init() from your instrumentation-client entrypoint.\"\n );\n }\n return instance;\n}\n\nfunction isEnabledByEnvironment(): boolean {\n try {\n if (typeof process === \"undefined\" || !process.env) {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === \"production\") {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === undefined) {\n return true;\n }\n return !!(\n (globalThis as Record<string, unknown>)[\"__INTERFERE_FORCE_ENABLE__\"] ||\n process.env[\"INTERFERE_FORCE_ENABLE\"]\n );\n } catch {\n return true;\n }\n}\n\nexport function init(opts: ClientOptions = {}): void {\n if (instance) {\n return;\n }\n\n if (!(opts.enabled ?? isEnabledByEnvironment())) {\n log.info(\n \"Disabled in non-production. Pass enabled: true to init() or set INTERFERE_FORCE_ENABLE=1.\"\n );\n return;\n }\n\n const buildId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_BUILD_ID__\"\n ] as string | undefined;\n\n const releaseId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_RELEASE_ID__\"\n ] as string | null | undefined;\n\n if (!buildId) {\n log.error(\n \"buildId not found — ensure withInterfere() is configured in \" +\n \"next.config and instrumentation-client.ts exists in your project root.\"\n );\n return;\n }\n\n instance = new Client(opts, buildId, releaseId ?? null);\n}\n\nexport async function close(): Promise<void> {\n if (!instance) {\n return;\n }\n\n await instance.dispose();\n instance = null;\n}\n\nexport const consent = {\n get(): ConsentState | null {\n return instance?.getConsent() ?? null;\n },\n\n set(value?: ConsentState): void {\n instance?.setConsent(value);\n },\n};\n\nexport function syncConsent(consentState: ConsentState | undefined): void {\n if (!instance) {\n return;\n }\n\n if (consentState) {\n instance.setConsent(consentState);\n return;\n }\n\n instance.resetConsent();\n}\n\nexport function flush(): void {\n instance?.flush();\n}\n\n/** @internal Test-only. Resets the module state so init() can be called again. */\nexport function _reset(): void {\n instance = null;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.mts","names":[],"sources":["../../src/internal/config.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"config.d.mts","names":[],"sources":["../../src/internal/config.ts"],"mappings":";;;iBAyBgB,cAAA,CAAA;EACd,MAAA,EAAQ,YAAA;EACR,MAAA,EAAQ,YAAA;EACR,OAAA,EAAS,YAAA;AAAA"}
|
package/dist/internal/config.mjs
CHANGED
|
@@ -4,8 +4,9 @@ const DEFAULT_PROXY_URL = "/api/interfere";
|
|
|
4
4
|
const DEFAULT_SESSION_PATH = "/v1/session";
|
|
5
5
|
const DEFAULT_CONFIG_PATH = "/v1/config";
|
|
6
6
|
function resolvePublicKey() {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const injected = globalThis["__INTERFERE_PUBLIC_KEY__"];
|
|
8
|
+
if (injected) return injected;
|
|
9
|
+
if (typeof process !== "undefined") return process.env["INTERFERE_PUBLIC_KEY"] ?? void 0;
|
|
9
10
|
}
|
|
10
11
|
function resolveTargets() {
|
|
11
12
|
const publicKey = resolvePublicKey();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.mjs","names":[],"sources":["../../src/internal/config.ts"],"sourcesContent":["import { API_PATHS, API_URL } from \"@interfere/constants/api\";\n\nimport type { IngestTarget } from \"../transport/http.js\";\n\nconst DEFAULT_PROXY_URL = \"/api/interfere\";\nconst DEFAULT_SESSION_PATH = \"/v1/session\";\nconst DEFAULT_CONFIG_PATH = \"/v1/config\";\n\nfunction resolvePublicKey(): string | undefined {\n
|
|
1
|
+
{"version":3,"file":"config.mjs","names":[],"sources":["../../src/internal/config.ts"],"sourcesContent":["import { API_PATHS, API_URL } from \"@interfere/constants/api\";\n\nimport type { IngestTarget } from \"../transport/http.js\";\n\nconst DEFAULT_PROXY_URL = \"/api/interfere\";\nconst DEFAULT_SESSION_PATH = \"/v1/session\";\nconst DEFAULT_CONFIG_PATH = \"/v1/config\";\n\nfunction resolvePublicKey(): string | undefined {\n // Vite plugin injects the key onto globalThis at build time\n const injected = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_PUBLIC_KEY__\"\n ] as string | undefined;\n if (injected) {\n return injected;\n }\n\n // Node / webpack / Next.js: read from process.env\n if (typeof process !== \"undefined\") {\n return process.env[\"INTERFERE_PUBLIC_KEY\"] ?? undefined;\n }\n\n return undefined;\n}\n\nexport function resolveTargets(): {\n config: IngestTarget;\n ingest: IngestTarget;\n session: IngestTarget;\n} {\n const publicKey = resolvePublicKey();\n const headers = new Headers({ \"content-type\": \"application/json\" });\n if (publicKey) {\n headers.set(\"x-interfere-pub-token\", publicKey);\n }\n\n const baseUrl = publicKey ? API_URL : DEFAULT_PROXY_URL;\n const sessionPath =\n (API_PATHS as { SESSION?: string }).SESSION ?? DEFAULT_SESSION_PATH;\n const configPath =\n (API_PATHS as { CONFIG?: string }).CONFIG ?? DEFAULT_CONFIG_PATH;\n\n return {\n config: {\n url: `${baseUrl}${configPath}`,\n headers,\n },\n ingest: {\n url: `${baseUrl}${API_PATHS.INGEST}`,\n headers,\n },\n session: {\n url: `${baseUrl}${sessionPath}`,\n headers,\n },\n };\n}\n"],"mappings":";;AAIA,MAAM,oBAAoB;AAC1B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAE5B,SAAS,mBAAuC;CAE9C,MAAM,WAAY,WAChB;AAEF,KAAI,SACF,QAAO;AAIT,KAAI,OAAO,YAAY,YACrB,QAAO,QAAQ,IAAI,2BAA2B,KAAA;;AAMlD,SAAgB,iBAId;CACA,MAAM,YAAY,kBAAkB;CACpC,MAAM,UAAU,IAAI,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AACnE,KAAI,UACF,SAAQ,IAAI,yBAAyB,UAAU;CAGjD,MAAM,UAAU,YAAY,UAAU;CACtC,MAAM,cACH,UAAmC,WAAW;AAIjD,QAAO;EACL,QAAQ;GACN,KAAK,GAAG,UAJT,UAAkC,UAAU;GAK3C;GACD;EACD,QAAQ;GACN,KAAK,GAAG,UAAU,UAAU;GAC5B;GACD;EACD,SAAS;GACP,KAAK,GAAG,UAAU;GAClB;GACD;EACF"}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { PluginContext } from "../plugins/lib/types.mjs";
|
|
2
2
|
import { PluginOverrides } from "../plugins/lib/loader.mjs";
|
|
3
3
|
import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
|
|
4
|
-
import { EventType } from "@interfere/types/sdk/envelope";
|
|
5
4
|
import { RemotePluginConfig } from "@interfere/types/sdk/remote-config";
|
|
5
|
+
import { EventType } from "@interfere/types/sdk/envelope";
|
|
6
6
|
|
|
7
7
|
//#region src/internal/plugin-runtime.d.ts
|
|
8
8
|
declare class PluginRuntime {
|
|
9
9
|
private readonly activeCleanups;
|
|
10
|
+
private readonly pending;
|
|
10
11
|
private readonly context;
|
|
11
12
|
private readonly features;
|
|
12
13
|
private remoteConfig;
|
|
13
14
|
private consentState;
|
|
14
15
|
private syncVersion;
|
|
16
|
+
private disposed;
|
|
15
17
|
constructor(context: PluginContext, overrides: PluginOverrides | undefined, initialConsent: ConsentState | undefined);
|
|
16
18
|
getConsent(): ConsentState | null;
|
|
17
19
|
setConsent(nextConsent?: ConsentState): void;
|
|
@@ -19,7 +21,7 @@ declare class PluginRuntime {
|
|
|
19
21
|
applyRemoteConfig(config: RemotePluginConfig): void;
|
|
20
22
|
canCapture(type: EventType): boolean;
|
|
21
23
|
start(): void;
|
|
22
|
-
dispose(): void
|
|
24
|
+
dispose(): Promise<void>;
|
|
23
25
|
private shouldEnablePlugin;
|
|
24
26
|
private deactivate;
|
|
25
27
|
private activate;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-runtime.d.mts","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"plugin-runtime.d.mts","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"mappings":";;;;;;;cA6Ba,aAAA;EAAA,iBACM,cAAA;EAAA,iBACA,OAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;EAAA,QACT,YAAA;EAAA,QACA,YAAA;EAAA,QACA,WAAA;EAAA,QACA,QAAA;cAGN,OAAA,EAAS,aAAA,EACT,SAAA,EAAW,eAAA,cACX,cAAA,EAAgB,YAAA;EAOlB,UAAA,CAAA,GAAc,YAAA;EAId,UAAA,CAAW,WAAA,GAAc,YAAA;EAUzB,YAAA,CAAA;EASA,iBAAA,CAAkB,MAAA,EAAQ,kBAAA;EAK1B,UAAA,CAAW,IAAA,EAAM,SAAA;EAQjB,KAAA,CAAA;EAWM,OAAA,CAAA,GAAW,OAAA;EAAA,QAWT,kBAAA;EAAA,QAQA,UAAA;EAAA,QAeM,QAAA;EAAA,QAoBN,IAAA;AAAA"}
|
|
@@ -3,15 +3,18 @@ import errorsPlugin from "../plugins/errors.mjs";
|
|
|
3
3
|
import { loadPlugin, resolveFeatures } from "../plugins/lib/loader.mjs";
|
|
4
4
|
import { getPluginConsentCategory, hasConsentChanged, isConsentAllowed, resolveGrantedConsent, shouldCaptureEvent } from "./consent.mjs";
|
|
5
5
|
import { PLUGIN_MANIFEST } from "@interfere/types/sdk/plugins/manifest";
|
|
6
|
+
import { EVENT_TYPE_TO_PLUGIN } from "@interfere/types/sdk/remote-config";
|
|
6
7
|
//#region src/internal/plugin-runtime.ts
|
|
7
8
|
const log = createLogger("plugin-runtime");
|
|
8
9
|
var PluginRuntime = class {
|
|
9
10
|
activeCleanups = /* @__PURE__ */ new Map();
|
|
11
|
+
pending = /* @__PURE__ */ new Set();
|
|
10
12
|
context;
|
|
11
13
|
features;
|
|
12
14
|
remoteConfig = {};
|
|
13
15
|
consentState;
|
|
14
16
|
syncVersion = 0;
|
|
17
|
+
disposed = false;
|
|
15
18
|
constructor(context, overrides, initialConsent) {
|
|
16
19
|
this.context = context;
|
|
17
20
|
this.features = resolveFeatures(overrides);
|
|
@@ -36,6 +39,8 @@ var PluginRuntime = class {
|
|
|
36
39
|
this.sync();
|
|
37
40
|
}
|
|
38
41
|
canCapture(type) {
|
|
42
|
+
const plugin = EVENT_TYPE_TO_PLUGIN[type];
|
|
43
|
+
if (plugin && this.remoteConfig[plugin] === false) return false;
|
|
39
44
|
return shouldCaptureEvent(type, this.consentState);
|
|
40
45
|
}
|
|
41
46
|
start() {
|
|
@@ -45,8 +50,11 @@ var PluginRuntime = class {
|
|
|
45
50
|
}
|
|
46
51
|
this.sync();
|
|
47
52
|
}
|
|
48
|
-
dispose() {
|
|
53
|
+
async dispose() {
|
|
54
|
+
this.disposed = true;
|
|
55
|
+
this.syncVersion += 1;
|
|
49
56
|
for (const key of this.activeCleanups.keys()) this.deactivate(key);
|
|
57
|
+
await Promise.allSettled(this.pending);
|
|
50
58
|
}
|
|
51
59
|
shouldEnablePlugin(key) {
|
|
52
60
|
return this.features[key] && this.remoteConfig[key] !== false && isConsentAllowed(getPluginConsentCategory(key), this.consentState);
|
|
@@ -73,6 +81,7 @@ var PluginRuntime = class {
|
|
|
73
81
|
this.activeCleanups.set(key, cleanup);
|
|
74
82
|
}
|
|
75
83
|
sync() {
|
|
84
|
+
if (this.disposed) return;
|
|
76
85
|
this.syncVersion += 1;
|
|
77
86
|
for (const plugin of PLUGIN_MANIFEST) {
|
|
78
87
|
if (this.shouldEnablePlugin(plugin.name)) {
|
|
@@ -83,9 +92,11 @@ var PluginRuntime = class {
|
|
|
83
92
|
}
|
|
84
93
|
continue;
|
|
85
94
|
}
|
|
86
|
-
this.activate(plugin.name).catch(() => {
|
|
95
|
+
const p = this.activate(plugin.name).catch(() => {
|
|
87
96
|
log.warn("non-critical plugin loading failed");
|
|
88
97
|
});
|
|
98
|
+
this.pending.add(p);
|
|
99
|
+
p.finally(() => this.pending.delete(p));
|
|
89
100
|
continue;
|
|
90
101
|
}
|
|
91
102
|
this.deactivate(plugin.name);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-runtime.mjs","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"sourcesContent":["import type { EventType } from \"@interfere/types/sdk/envelope\";\nimport {\n type ConsentState,\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\nimport type
|
|
1
|
+
{"version":3,"file":"plugin-runtime.mjs","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"sourcesContent":["import type { EventType } from \"@interfere/types/sdk/envelope\";\nimport {\n type ConsentState,\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\nimport {\n EVENT_TYPE_TO_PLUGIN,\n type RemotePluginConfig,\n} from \"@interfere/types/sdk/remote-config\";\n\nimport errorsPlugin from \"../plugins/errors.js\";\nimport {\n loadPlugin,\n type PluginOverrides,\n resolveFeatures,\n} from \"../plugins/lib/loader.js\";\nimport type { PluginCleanup, PluginContext } from \"../plugins/lib/types.js\";\nimport { createLogger } from \"../util/log.js\";\nimport {\n getPluginConsentCategory,\n hasConsentChanged,\n isConsentAllowed,\n resolveGrantedConsent,\n shouldCaptureEvent,\n} from \"./consent.js\";\n\nconst log = createLogger(\"plugin-runtime\");\n\nexport class PluginRuntime {\n private readonly activeCleanups = new Map<PluginKey, PluginCleanup>();\n private readonly pending = new Set<Promise<void>>();\n private readonly context: PluginContext;\n private readonly features: Record<PluginKey, boolean>;\n private remoteConfig: RemotePluginConfig = {};\n private consentState: ConsentState | null;\n private syncVersion = 0;\n private disposed = false;\n\n constructor(\n context: PluginContext,\n overrides: PluginOverrides | undefined,\n initialConsent: ConsentState | undefined\n ) {\n this.context = context;\n this.features = resolveFeatures(overrides);\n this.consentState = initialConsent ?? null;\n }\n\n getConsent(): ConsentState | null {\n return this.consentState;\n }\n\n setConsent(nextConsent?: ConsentState): void {\n const nextState = resolveGrantedConsent(nextConsent);\n if (!hasConsentChanged(this.consentState, nextState)) {\n return;\n }\n\n this.consentState = nextState;\n this.sync();\n }\n\n resetConsent(): void {\n if (!hasConsentChanged(this.consentState, null)) {\n return;\n }\n\n this.consentState = null;\n this.sync();\n }\n\n applyRemoteConfig(config: RemotePluginConfig): void {\n this.remoteConfig = config;\n this.sync();\n }\n\n canCapture(type: EventType): boolean {\n const plugin = EVENT_TYPE_TO_PLUGIN[type];\n if (plugin && this.remoteConfig[plugin] === false) {\n return false;\n }\n return shouldCaptureEvent(type, this.consentState);\n }\n\n start(): void {\n if (this.shouldEnablePlugin(\"errors\")) {\n const cleanup = errorsPlugin.setup(this.context);\n if (cleanup) {\n this.activeCleanups.set(\"errors\", cleanup);\n }\n }\n\n this.sync();\n }\n\n async dispose(): Promise<void> {\n this.disposed = true;\n this.syncVersion += 1;\n\n for (const key of this.activeCleanups.keys()) {\n this.deactivate(key);\n }\n\n await Promise.allSettled(this.pending);\n }\n\n private shouldEnablePlugin(key: PluginKey): boolean {\n return (\n this.features[key] &&\n this.remoteConfig[key] !== false &&\n isConsentAllowed(getPluginConsentCategory(key), this.consentState)\n );\n }\n\n private deactivate(key: PluginKey): void {\n const cleanup = this.activeCleanups.get(key);\n if (!cleanup) {\n return;\n }\n\n try {\n cleanup();\n } catch {\n log.warn(\"cleanup failed for %s\", key);\n }\n\n this.activeCleanups.delete(key);\n }\n\n private async activate(key: PluginKey): Promise<void> {\n if (this.activeCleanups.has(key) || !this.shouldEnablePlugin(key)) {\n return;\n }\n\n const version = this.syncVersion;\n const cleanup = await loadPlugin(key, this.context);\n if (!cleanup) {\n return;\n }\n\n const staleSync = version !== this.syncVersion;\n if (staleSync || !this.shouldEnablePlugin(key)) {\n cleanup();\n return;\n }\n\n this.activeCleanups.set(key, cleanup);\n }\n\n private sync(): void {\n if (this.disposed) {\n return;\n }\n\n this.syncVersion += 1;\n\n for (const plugin of PLUGIN_MANIFEST) {\n if (this.shouldEnablePlugin(plugin.name)) {\n if (plugin.name === \"errors\") {\n if (!this.activeCleanups.has(\"errors\")) {\n const cleanup = errorsPlugin.setup(this.context);\n if (cleanup) {\n this.activeCleanups.set(\"errors\", cleanup);\n }\n }\n continue;\n }\n\n const p = this.activate(plugin.name).catch(() => {\n log.warn(\"non-critical plugin loading failed\");\n });\n this.pending.add(p);\n p.finally(() => this.pending.delete(p));\n continue;\n }\n\n this.deactivate(plugin.name);\n }\n }\n}\n"],"mappings":";;;;;;;AA2BA,MAAM,MAAM,aAAa,iBAAiB;AAE1C,IAAa,gBAAb,MAA2B;CACzB,iCAAkC,IAAI,KAA+B;CACrE,0BAA2B,IAAI,KAAoB;CACnD;CACA;CACA,eAA2C,EAAE;CAC7C;CACA,cAAsB;CACtB,WAAmB;CAEnB,YACE,SACA,WACA,gBACA;AACA,OAAK,UAAU;AACf,OAAK,WAAW,gBAAgB,UAAU;AAC1C,OAAK,eAAe,kBAAkB;;CAGxC,aAAkC;AAChC,SAAO,KAAK;;CAGd,WAAW,aAAkC;EAC3C,MAAM,YAAY,sBAAsB,YAAY;AACpD,MAAI,CAAC,kBAAkB,KAAK,cAAc,UAAU,CAClD;AAGF,OAAK,eAAe;AACpB,OAAK,MAAM;;CAGb,eAAqB;AACnB,MAAI,CAAC,kBAAkB,KAAK,cAAc,KAAK,CAC7C;AAGF,OAAK,eAAe;AACpB,OAAK,MAAM;;CAGb,kBAAkB,QAAkC;AAClD,OAAK,eAAe;AACpB,OAAK,MAAM;;CAGb,WAAW,MAA0B;EACnC,MAAM,SAAS,qBAAqB;AACpC,MAAI,UAAU,KAAK,aAAa,YAAY,MAC1C,QAAO;AAET,SAAO,mBAAmB,MAAM,KAAK,aAAa;;CAGpD,QAAc;AACZ,MAAI,KAAK,mBAAmB,SAAS,EAAE;GACrC,MAAM,UAAU,aAAa,MAAM,KAAK,QAAQ;AAChD,OAAI,QACF,MAAK,eAAe,IAAI,UAAU,QAAQ;;AAI9C,OAAK,MAAM;;CAGb,MAAM,UAAyB;AAC7B,OAAK,WAAW;AAChB,OAAK,eAAe;AAEpB,OAAK,MAAM,OAAO,KAAK,eAAe,MAAM,CAC1C,MAAK,WAAW,IAAI;AAGtB,QAAM,QAAQ,WAAW,KAAK,QAAQ;;CAGxC,mBAA2B,KAAyB;AAClD,SACE,KAAK,SAAS,QACd,KAAK,aAAa,SAAS,SAC3B,iBAAiB,yBAAyB,IAAI,EAAE,KAAK,aAAa;;CAItE,WAAmB,KAAsB;EACvC,MAAM,UAAU,KAAK,eAAe,IAAI,IAAI;AAC5C,MAAI,CAAC,QACH;AAGF,MAAI;AACF,YAAS;UACH;AACN,OAAI,KAAK,yBAAyB,IAAI;;AAGxC,OAAK,eAAe,OAAO,IAAI;;CAGjC,MAAc,SAAS,KAA+B;AACpD,MAAI,KAAK,eAAe,IAAI,IAAI,IAAI,CAAC,KAAK,mBAAmB,IAAI,CAC/D;EAGF,MAAM,UAAU,KAAK;EACrB,MAAM,UAAU,MAAM,WAAW,KAAK,KAAK,QAAQ;AACnD,MAAI,CAAC,QACH;AAIF,MADkB,YAAY,KAAK,eAClB,CAAC,KAAK,mBAAmB,IAAI,EAAE;AAC9C,YAAS;AACT;;AAGF,OAAK,eAAe,IAAI,KAAK,QAAQ;;CAGvC,OAAqB;AACnB,MAAI,KAAK,SACP;AAGF,OAAK,eAAe;AAEpB,OAAK,MAAM,UAAU,iBAAiB;AACpC,OAAI,KAAK,mBAAmB,OAAO,KAAK,EAAE;AACxC,QAAI,OAAO,SAAS,UAAU;AAC5B,SAAI,CAAC,KAAK,eAAe,IAAI,SAAS,EAAE;MACtC,MAAM,UAAU,aAAa,MAAM,KAAK,QAAQ;AAChD,UAAI,QACF,MAAK,eAAe,IAAI,UAAU,QAAQ;;AAG9C;;IAGF,MAAM,IAAI,KAAK,SAAS,OAAO,KAAK,CAAC,YAAY;AAC/C,SAAI,KAAK,qCAAqC;MAC9C;AACF,SAAK,QAAQ,IAAI,EAAE;AACnB,MAAE,cAAc,KAAK,QAAQ,OAAO,EAAE,CAAC;AACvC;;AAGF,QAAK,WAAW,OAAO,KAAK"}
|
package/dist/package.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;iBAyFgB,SAAA,CAAU,aAAA,EAAe,YAAA;AAAA,cAY5B,MAAA;EAQZ,WAAA;EAAA,SAAA;AAAA;AAAA,cAEY,OAAA;EAYZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIW,cAAA,GAAiB,OAAA;;;iBAgDrB,QAAA,CAAA"}
|
package/dist/tracking/api.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLogger } from "../util/log.mjs";
|
|
2
2
|
import { getDeviceId, getFpHash, initDevice, whenDeviceReady, whenFingerprintReady } from "./device.mjs";
|
|
3
3
|
import { buildHeaders } from "../transport/http.mjs";
|
|
4
|
+
import { detectCountryCode, resetGeo } from "./geo.mjs";
|
|
4
5
|
import { SessionManager } from "./session.mjs";
|
|
5
6
|
//#region src/tracking/api.ts
|
|
6
7
|
const log = createLogger("tracking");
|
|
@@ -10,10 +11,12 @@ let currentIdentity = null;
|
|
|
10
11
|
let identifiedSessionId = null;
|
|
11
12
|
let syncedSessionId = null;
|
|
12
13
|
let syncAttemptMs = 0;
|
|
14
|
+
let generation = 0;
|
|
13
15
|
const SYNC_COOLDOWN_MS = 5e3;
|
|
14
16
|
function syncSession(sessionId, deviceId, fpHash) {
|
|
15
17
|
if (!target) return;
|
|
16
18
|
syncAttemptMs = Date.now();
|
|
19
|
+
syncedSessionId = sessionId;
|
|
17
20
|
fetch(target.url, {
|
|
18
21
|
method: "POST",
|
|
19
22
|
headers: buildHeaders(target.headers),
|
|
@@ -25,8 +28,9 @@ function syncSession(sessionId, deviceId, fpHash) {
|
|
|
25
28
|
keepalive: true,
|
|
26
29
|
signal: AbortSignal.timeout(1e4)
|
|
27
30
|
}).then((res) => {
|
|
28
|
-
if (res.ok) syncedSessionId =
|
|
31
|
+
if (!res.ok) syncedSessionId = null;
|
|
29
32
|
}).catch(() => {
|
|
33
|
+
syncedSessionId = null;
|
|
30
34
|
log.warn("session sync failed, will retry");
|
|
31
35
|
});
|
|
32
36
|
}
|
|
@@ -39,13 +43,16 @@ async function onRotate(sessionId) {
|
|
|
39
43
|
syncedSessionId = null;
|
|
40
44
|
syncAttemptMs = Date.now();
|
|
41
45
|
if (!target) return;
|
|
46
|
+
const gen = generation;
|
|
42
47
|
const [deviceId, fpHash] = await Promise.all([whenDeviceReady(), whenFingerprintReady()]);
|
|
48
|
+
if (gen !== generation) return;
|
|
43
49
|
log.debug("POST session %s (device=%s fp=%s)", sessionId, deviceId ?? "pending", fpHash ?? "none");
|
|
44
50
|
syncSession(sessionId, deviceId, fpHash);
|
|
45
51
|
}
|
|
46
52
|
function bootstrap(sessionTarget) {
|
|
47
53
|
target = sessionTarget;
|
|
48
54
|
initDevice();
|
|
55
|
+
detectCountryCode();
|
|
49
56
|
mgr = new SessionManager((id) => {
|
|
50
57
|
onRotate(id).catch(() => {});
|
|
51
58
|
});
|
|
@@ -81,8 +88,13 @@ const identity = {
|
|
|
81
88
|
}
|
|
82
89
|
currentIdentity = params;
|
|
83
90
|
identifiedSessionId = sessionId;
|
|
84
|
-
const
|
|
85
|
-
|
|
91
|
+
const gen = generation;
|
|
92
|
+
const [deviceId, fpHash, country] = await Promise.all([
|
|
93
|
+
whenDeviceReady(),
|
|
94
|
+
whenFingerprintReady(),
|
|
95
|
+
detectCountryCode()
|
|
96
|
+
]);
|
|
97
|
+
if (gen !== generation || !target) {
|
|
86
98
|
identifiedSessionId = null;
|
|
87
99
|
return;
|
|
88
100
|
}
|
|
@@ -94,7 +106,8 @@ const identity = {
|
|
|
94
106
|
sessionId,
|
|
95
107
|
deviceId,
|
|
96
108
|
fpHash,
|
|
97
|
-
...params
|
|
109
|
+
...params,
|
|
110
|
+
...country && { country }
|
|
98
111
|
}),
|
|
99
112
|
keepalive: true,
|
|
100
113
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -109,7 +122,9 @@ const identity = {
|
|
|
109
122
|
}
|
|
110
123
|
};
|
|
111
124
|
function teardown() {
|
|
125
|
+
generation += 1;
|
|
112
126
|
identity.clear();
|
|
127
|
+
resetGeo();
|
|
113
128
|
syncedSessionId = null;
|
|
114
129
|
syncAttemptMs = 0;
|
|
115
130
|
mgr = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport { buildHeaders, type IngestTarget } from \"../transport/http.js\";\nimport { createLogger } from \"../util/log.js\";\nimport {\n getDeviceId,\n getFpHash,\n initDevice,\n whenDeviceReady,\n whenFingerprintReady,\n} from \"./device.js\";\nimport { SessionManager } from \"./session.js\";\n\nconst log = createLogger(\"tracking\");\n\nlet mgr: SessionManager | null = null;\nlet target: IngestTarget | null = null;\nlet currentIdentity: IdentifyParams | null = null;\nlet identifiedSessionId: string | null = null;\n\nlet syncedSessionId: string | null = null;\nlet syncAttemptMs = 0;\nconst SYNC_COOLDOWN_MS = 5000;\n\nfunction syncSession(\n sessionId: string,\n deviceId: string | null,\n fpHash: string | null\n): void {\n if (!target) {\n return;\n }\n\n syncAttemptMs = Date.now();\n\n fetch(target.url, {\n method: \"POST\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({ sessionId, deviceId, fpHash }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (res.ok) {\n syncedSessionId =
|
|
1
|
+
{"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport { buildHeaders, type IngestTarget } from \"../transport/http.js\";\nimport { createLogger } from \"../util/log.js\";\nimport {\n getDeviceId,\n getFpHash,\n initDevice,\n whenDeviceReady,\n whenFingerprintReady,\n} from \"./device.js\";\nimport { detectCountryCode, resetGeo } from \"./geo.js\";\nimport { SessionManager } from \"./session.js\";\n\nconst log = createLogger(\"tracking\");\n\nlet mgr: SessionManager | null = null;\nlet target: IngestTarget | null = null;\nlet currentIdentity: IdentifyParams | null = null;\nlet identifiedSessionId: string | null = null;\n\nlet syncedSessionId: string | null = null;\nlet syncAttemptMs = 0;\nlet generation = 0;\nconst SYNC_COOLDOWN_MS = 5000;\n\nfunction syncSession(\n sessionId: string,\n deviceId: string | null,\n fpHash: string | null\n): void {\n if (!target) {\n return;\n }\n\n syncAttemptMs = Date.now();\n syncedSessionId = sessionId;\n\n fetch(target.url, {\n method: \"POST\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({ sessionId, deviceId, fpHash }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n syncedSessionId = null;\n }\n })\n .catch(() => {\n syncedSessionId = null;\n log.warn(\"session sync failed, will retry\");\n });\n}\n\nfunction ensureSynced(sessionId: string): void {\n if (syncedSessionId === sessionId) {\n return;\n }\n if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) {\n return;\n }\n syncSession(sessionId, getDeviceId(), getFpHash());\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n syncedSessionId = null;\n syncAttemptMs = Date.now();\n if (!target) {\n return;\n }\n const gen = generation;\n const [deviceId, fpHash] = await Promise.all([\n whenDeviceReady(),\n whenFingerprintReady(),\n ]);\n if (gen !== generation) {\n return;\n }\n log.debug(\n \"POST session %s (device=%s fp=%s)\",\n sessionId,\n deviceId ?? \"pending\",\n fpHash ?? \"none\"\n );\n syncSession(sessionId, deviceId, fpHash);\n}\n\nexport function bootstrap(sessionTarget: IngestTarget): void {\n target = sessionTarget;\n initDevice();\n detectCountryCode();\n\n mgr = new SessionManager((id) => {\n onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n}\n\nexport const device = {\n getDeviceId(): string | null {\n return getDeviceId();\n },\n\n getFpHash(): string | null {\n return getFpHash();\n },\n};\n\nexport const session = {\n getId(): string | null {\n const id = mgr?.getSessionId() ?? null;\n if (id) {\n ensureSynced(id);\n }\n return id;\n },\n\n getWindowId(): string | null {\n return mgr?.getWindowId() ?? null;\n },\n};\n\nexport const identity = {\n get(): IdentifyParams | null {\n return currentIdentity;\n },\n\n async set(params: IdentifyParams): Promise<void> {\n if (!(mgr && target)) {\n return;\n }\n const sessionId = mgr.getSessionId();\n if (identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n currentIdentity = params;\n identifiedSessionId = sessionId;\n\n const gen = generation;\n const [deviceId, fpHash, country] = await Promise.all([\n whenDeviceReady(),\n whenFingerprintReady(),\n detectCountryCode(),\n ]);\n if (gen !== generation || !target) {\n identifiedSessionId = null;\n return;\n }\n log.info(\"PUT session %s → user %s\", sessionId, params.identifier);\n fetch(target.url, {\n method: \"PUT\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({\n sessionId,\n deviceId,\n fpHash,\n ...params,\n ...(country && { country }),\n }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n }).catch(() => {\n identifiedSessionId = null;\n log.warn(\"identify failed for session %s\", sessionId);\n });\n },\n\n clear(): void {\n currentIdentity = null;\n identifiedSessionId = null;\n },\n};\n\nexport function teardown(): void {\n generation += 1;\n identity.clear();\n resetGeo();\n syncedSessionId = null;\n syncAttemptMs = 0;\n mgr = null;\n target = null;\n}\n"],"mappings":";;;;;;AAcA,MAAM,MAAM,aAAa,WAAW;AAEpC,IAAI,MAA6B;AACjC,IAAI,SAA8B;AAClC,IAAI,kBAAyC;AAC7C,IAAI,sBAAqC;AAEzC,IAAI,kBAAiC;AACrC,IAAI,gBAAgB;AACpB,IAAI,aAAa;AACjB,MAAM,mBAAmB;AAEzB,SAAS,YACP,WACA,UACA,QACM;AACN,KAAI,CAAC,OACH;AAGF,iBAAgB,KAAK,KAAK;AAC1B,mBAAkB;AAElB,OAAM,OAAO,KAAK;EAChB,QAAQ;EACR,SAAS,aAAa,OAAO,QAAQ;EACrC,MAAM,KAAK,UAAU;GAAE;GAAW;GAAU;GAAQ,CAAC;EACrD,WAAW;EACX,QAAQ,YAAY,QAAQ,IAAO;EACpC,CAAC,CACC,MAAM,QAAQ;AACb,MAAI,CAAC,IAAI,GACP,mBAAkB;GAEpB,CACD,YAAY;AACX,oBAAkB;AAClB,MAAI,KAAK,kCAAkC;GAC3C;;AAGN,SAAS,aAAa,WAAyB;AAC7C,KAAI,oBAAoB,UACtB;AAEF,KAAI,KAAK,KAAK,GAAG,gBAAgB,iBAC/B;AAEF,aAAY,WAAW,aAAa,EAAE,WAAW,CAAC;;AAGpD,eAAe,SAAS,WAAkC;AACxD,mBAAkB;AAClB,iBAAgB,KAAK,KAAK;AAC1B,KAAI,CAAC,OACH;CAEF,MAAM,MAAM;CACZ,MAAM,CAAC,UAAU,UAAU,MAAM,QAAQ,IAAI,CAC3C,iBAAiB,EACjB,sBAAsB,CACvB,CAAC;AACF,KAAI,QAAQ,WACV;AAEF,KAAI,MACF,qCACA,WACA,YAAY,WACZ,UAAU,OACX;AACD,aAAY,WAAW,UAAU,OAAO;;AAG1C,SAAgB,UAAU,eAAmC;AAC3D,UAAS;AACT,aAAY;AACZ,oBAAmB;AAEnB,OAAM,IAAI,gBAAgB,OAAO;AAC/B,WAAS,GAAG,CAAC,YAAY,GAEvB;GACF;;AAGJ,MAAa,SAAS;CACpB,cAA6B;AAC3B,SAAO,aAAa;;CAGtB,YAA2B;AACzB,SAAO,WAAW;;CAErB;AAED,MAAa,UAAU;CACrB,QAAuB;EACrB,MAAM,KAAK,KAAK,cAAc,IAAI;AAClC,MAAI,GACF,cAAa,GAAG;AAElB,SAAO;;CAGT,cAA6B;AAC3B,SAAO,KAAK,aAAa,IAAI;;CAEhC;AAED,MAAa,WAAW;CACtB,MAA6B;AAC3B,SAAO;;CAGT,MAAM,IAAI,QAAuC;AAC/C,MAAI,EAAE,OAAO,QACX;EAEF,MAAM,YAAY,IAAI,cAAc;AACpC,MAAI,wBAAwB,WAAW;AACrC,OAAI,MAAM,8CAA8C,UAAU;AAClE;;AAGF,oBAAkB;AAClB,wBAAsB;EAEtB,MAAM,MAAM;EACZ,MAAM,CAAC,UAAU,QAAQ,WAAW,MAAM,QAAQ,IAAI;GACpD,iBAAiB;GACjB,sBAAsB;GACtB,mBAAmB;GACpB,CAAC;AACF,MAAI,QAAQ,cAAc,CAAC,QAAQ;AACjC,yBAAsB;AACtB;;AAEF,MAAI,KAAK,4BAA4B,WAAW,OAAO,WAAW;AAClE,QAAM,OAAO,KAAK;GAChB,QAAQ;GACR,SAAS,aAAa,OAAO,QAAQ;GACrC,MAAM,KAAK,UAAU;IACnB;IACA;IACA;IACA,GAAG;IACH,GAAI,WAAW,EAAE,SAAS;IAC3B,CAAC;GACF,WAAW;GACX,QAAQ,YAAY,QAAQ,IAAO;GACpC,CAAC,CAAC,YAAY;AACb,yBAAsB;AACtB,OAAI,KAAK,kCAAkC,UAAU;IACrD;;CAGJ,QAAc;AACZ,oBAAkB;AAClB,wBAAsB;;CAEzB;AAED,SAAgB,WAAiB;AAC/B,eAAc;AACd,UAAS,OAAO;AAChB,WAAU;AACV,mBAAkB;AAClB,iBAAgB;AAChB,OAAM;AACN,UAAS"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geo.d.mts","names":[],"sources":["../../src/tracking/geo.ts"],"mappings":";iBAwCgB,iBAAA,CAAA,GAAqB,OAAA;AAAA,iBAuBrB,QAAA,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cloudflareTraceSchema } from "@interfere/types/sdk/geo";
|
|
2
|
+
//#region src/tracking/geo.ts
|
|
3
|
+
const CF_TRACE_URL = "https://cloudflare.com/cdn-cgi/trace";
|
|
4
|
+
let cached = null;
|
|
5
|
+
let pending = null;
|
|
6
|
+
let failed = false;
|
|
7
|
+
function parseTrace(text) {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (const line of text.split("\n")) {
|
|
10
|
+
const idx = line.indexOf("=");
|
|
11
|
+
if (idx > 0) result[line.slice(0, idx)] = line.slice(idx + 1);
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
async function fetchCountryCode() {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(CF_TRACE_URL, { signal: AbortSignal.timeout(3e3) });
|
|
18
|
+
if (!res.ok) return null;
|
|
19
|
+
const raw = parseTrace(await res.text());
|
|
20
|
+
const trace = cloudflareTraceSchema.safeParse(raw);
|
|
21
|
+
if (!trace.success) return null;
|
|
22
|
+
return trace.data.loc.toUpperCase();
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function detectCountryCode() {
|
|
28
|
+
if (cached) return Promise.resolve(cached);
|
|
29
|
+
if (failed) return Promise.resolve(null);
|
|
30
|
+
if (!pending) pending = fetchCountryCode().then((code) => {
|
|
31
|
+
cached = code;
|
|
32
|
+
if (!code) failed = true;
|
|
33
|
+
pending = null;
|
|
34
|
+
return code;
|
|
35
|
+
});
|
|
36
|
+
return pending;
|
|
37
|
+
}
|
|
38
|
+
function resetGeo() {
|
|
39
|
+
cached = null;
|
|
40
|
+
pending = null;
|
|
41
|
+
failed = false;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { detectCountryCode, resetGeo };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geo.mjs","names":[],"sources":["../../src/tracking/geo.ts"],"sourcesContent":["import { cloudflareTraceSchema } from \"@interfere/types/sdk/geo\";\n\nconst CF_TRACE_URL = \"https://cloudflare.com/cdn-cgi/trace\";\n\nlet cached: string | null = null;\nlet pending: Promise<string | null> | null = null;\nlet failed = false;\n\nfunction parseTrace(text: string): Record<string, string> {\n const result: Record<string, string> = {};\n for (const line of text.split(\"\\n\")) {\n const idx = line.indexOf(\"=\");\n if (idx > 0) {\n result[line.slice(0, idx)] = line.slice(idx + 1);\n }\n }\n return result;\n}\n\nasync function fetchCountryCode(): Promise<string | null> {\n try {\n const res = await fetch(CF_TRACE_URL, {\n signal: AbortSignal.timeout(3000),\n });\n if (!res.ok) {\n return null;\n }\n\n const raw = parseTrace(await res.text());\n const trace = cloudflareTraceSchema.safeParse(raw);\n if (!trace.success) {\n return null;\n }\n\n return trace.data.loc.toUpperCase();\n } catch {\n return null;\n }\n}\n\nexport function detectCountryCode(): Promise<string | null> {\n if (cached) {\n return Promise.resolve(cached);\n }\n\n if (failed) {\n return Promise.resolve(null);\n }\n\n if (!pending) {\n pending = fetchCountryCode().then((code) => {\n cached = code;\n if (!code) {\n failed = true;\n }\n pending = null;\n return code;\n });\n }\n\n return pending;\n}\n\nexport function resetGeo(): void {\n cached = null;\n pending = null;\n failed = false;\n}\n"],"mappings":";;AAEA,MAAM,eAAe;AAErB,IAAI,SAAwB;AAC5B,IAAI,UAAyC;AAC7C,IAAI,SAAS;AAEb,SAAS,WAAW,MAAsC;CACxD,MAAM,SAAiC,EAAE;AACzC,MAAK,MAAM,QAAQ,KAAK,MAAM,KAAK,EAAE;EACnC,MAAM,MAAM,KAAK,QAAQ,IAAI;AAC7B,MAAI,MAAM,EACR,QAAO,KAAK,MAAM,GAAG,IAAI,IAAI,KAAK,MAAM,MAAM,EAAE;;AAGpD,QAAO;;AAGT,eAAe,mBAA2C;AACxD,KAAI;EACF,MAAM,MAAM,MAAM,MAAM,cAAc,EACpC,QAAQ,YAAY,QAAQ,IAAK,EAClC,CAAC;AACF,MAAI,CAAC,IAAI,GACP,QAAO;EAGT,MAAM,MAAM,WAAW,MAAM,IAAI,MAAM,CAAC;EACxC,MAAM,QAAQ,sBAAsB,UAAU,IAAI;AAClD,MAAI,CAAC,MAAM,QACT,QAAO;AAGT,SAAO,MAAM,KAAK,IAAI,aAAa;SAC7B;AACN,SAAO;;;AAIX,SAAgB,oBAA4C;AAC1D,KAAI,OACF,QAAO,QAAQ,QAAQ,OAAO;AAGhC,KAAI,OACF,QAAO,QAAQ,QAAQ,KAAK;AAG9B,KAAI,CAAC,QACH,WAAU,kBAAkB,CAAC,MAAM,SAAS;AAC1C,WAAS;AACT,MAAI,CAAC,KACH,UAAS;AAEX,YAAU;AACV,SAAO;GACP;AAGJ,QAAO;;AAGT,SAAgB,WAAiB;AAC/B,UAAS;AACT,WAAU;AACV,UAAS"}
|
|
@@ -5,6 +5,8 @@ import { Envelope } from "@interfere/types/sdk/envelope";
|
|
|
5
5
|
interface QueueOptions {
|
|
6
6
|
batchMs?: number;
|
|
7
7
|
batchSize?: number;
|
|
8
|
+
/** @internal Test-only. Override the service worker check. */
|
|
9
|
+
isServiceWorkerActive?: () => boolean;
|
|
8
10
|
maxBufferSize?: number;
|
|
9
11
|
transport: HttpTransport;
|
|
10
12
|
}
|
|
@@ -18,6 +20,7 @@ declare class BatchQueue {
|
|
|
18
20
|
private readonly batchMs;
|
|
19
21
|
private readonly maxBufferSize;
|
|
20
22
|
private readonly transport;
|
|
23
|
+
private readonly isServiceWorkerActive;
|
|
21
24
|
constructor(opts: QueueOptions);
|
|
22
25
|
start(): void;
|
|
23
26
|
enqueue(envelope: Envelope): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.d.mts","names":[],"sources":["../../src/transport/queue.ts"],"mappings":";;;;UAciB,YAAA;EACf,OAAA;EACA,SAAA;EACA,aAAA;EACA,SAAA,EAAW,aAAA;AAAA;AAAA,cAGA,UAAA;EAAA,iBACM,MAAA;EAAA,QACT,KAAA;EAAA,QACA,QAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,iBACS,SAAA;EAAA,iBACA,OAAA;EAAA,iBACA,aAAA;EAAA,iBACA,SAAA;cAEL,IAAA,EAAM,YAAA;
|
|
1
|
+
{"version":3,"file":"queue.d.mts","names":[],"sources":["../../src/transport/queue.ts"],"mappings":";;;;UAciB,YAAA;EACf,OAAA;EACA,SAAA;EAF2B;EAI3B,qBAAA;EACA,aAAA;EACA,SAAA,EAAW,aAAA;AAAA;AAAA,cAGA,UAAA;EAAA,iBACM,MAAA;EAAA,QACT,KAAA;EAAA,QACA,QAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,iBACS,SAAA;EAAA,iBACA,OAAA;EAAA,iBACA,aAAA;EAAA,iBACA,SAAA;EAAA,iBACA,qBAAA;cAEL,IAAA,EAAM,YAAA;EAQlB,KAAA,CAAA;EAeA,OAAA,CAAQ,QAAA,EAAU,QAAA;EAclB,KAAA,CAAA;EA2CA,OAAA,CAAA;EAAA,QAiBQ,KAAA;EAAA,iBASS,kBAAA;EAAA,iBAMA,cAAA;AAAA"}
|
package/dist/transport/queue.mjs
CHANGED
|
@@ -17,11 +17,13 @@ var BatchQueue = class {
|
|
|
17
17
|
batchMs;
|
|
18
18
|
maxBufferSize;
|
|
19
19
|
transport;
|
|
20
|
+
isServiceWorkerActive;
|
|
20
21
|
constructor(opts) {
|
|
21
22
|
this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
22
23
|
this.batchMs = opts.batchMs ?? DEFAULT_BATCH_MS;
|
|
23
24
|
this.maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
|
|
24
25
|
this.transport = opts.transport;
|
|
26
|
+
this.isServiceWorkerActive = opts.isServiceWorkerActive ?? hasServiceWorker;
|
|
25
27
|
}
|
|
26
28
|
start() {
|
|
27
29
|
if (this.timer) return;
|
|
@@ -55,7 +57,7 @@ var BatchQueue = class {
|
|
|
55
57
|
if (this.failures > 0) log.info("send recovered after %d failures", this.failures);
|
|
56
58
|
this.failures = 0;
|
|
57
59
|
}).catch(() => {
|
|
58
|
-
if (!
|
|
60
|
+
if (!this.isServiceWorkerActive()) this.buffer.unshift(...batch);
|
|
59
61
|
this.failures++;
|
|
60
62
|
if (this.failures >= BREAKER_THRESHOLD) {
|
|
61
63
|
this.pausedUntil = Date.now() + BREAKER_COOLDOWN_MS;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.mjs","names":[],"sources":["../../src/transport/queue.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { createLogger } from \"../util/log.js\";\nimport { type HttpTransport, hasServiceWorker } from \"./http.js\";\n\nconst log = createLogger(\"queue\");\n\nconst DEFAULT_BATCH_SIZE = 10;\nconst DEFAULT_BATCH_MS = 250;\nconst DEFAULT_MAX_BUFFER_SIZE = 1000;\n\nconst BREAKER_THRESHOLD = 5;\nconst BREAKER_COOLDOWN_MS = 30_000;\n\nexport interface QueueOptions {\n batchMs?: number;\n batchSize?: number;\n maxBufferSize?: number;\n transport: HttpTransport;\n}\n\nexport class BatchQueue {\n private readonly buffer: Envelope[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private failures = 0;\n private pausedUntil = 0;\n private readonly batchSize: number;\n private readonly batchMs: number;\n private readonly maxBufferSize: number;\n private readonly transport: HttpTransport;\n\n constructor(opts: QueueOptions) {\n this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;\n this.batchMs = opts.batchMs ?? DEFAULT_BATCH_MS;\n this.maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n this.transport = opts.transport;\n }\n\n start(): void {\n if (this.timer) {\n return;\n }\n\n this.timer = setInterval(() => {\n this.flush();\n }, this.batchMs);\n\n if (typeof globalThis.addEventListener === \"function\") {\n globalThis.addEventListener(\"visibilitychange\", this.onVisibilityChange);\n globalThis.addEventListener(\"beforeunload\", this.onBeforeUnload);\n }\n }\n\n enqueue(envelope: Envelope): void {\n this.buffer.push(envelope);\n\n if (this.buffer.length > this.maxBufferSize) {\n const overflow = this.buffer.length - this.maxBufferSize;\n this.buffer.splice(0, overflow);\n log.warn(\"buffer full, dropped %d oldest envelopes\", overflow);\n }\n\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n flush(): void {\n if (this.flushing || this.buffer.length === 0) {\n return;\n }\n\n if (this.failures >= BREAKER_THRESHOLD && Date.now() < this.pausedUntil) {\n return;\n }\n\n this.flushing = true;\n const batch = this.buffer.splice(0, this.batchSize);\n const meta = {\n retryCount: this.failures,\n queueDepth: this.buffer.length,\n };\n\n this.transport\n .send(batch, meta)\n .then(() => {\n if (this.failures > 0) {\n log.info(\"send recovered after %d failures\", this.failures);\n }\n this.failures = 0;\n })\n .catch(() => {\n if (!
|
|
1
|
+
{"version":3,"file":"queue.mjs","names":[],"sources":["../../src/transport/queue.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { createLogger } from \"../util/log.js\";\nimport { type HttpTransport, hasServiceWorker } from \"./http.js\";\n\nconst log = createLogger(\"queue\");\n\nconst DEFAULT_BATCH_SIZE = 10;\nconst DEFAULT_BATCH_MS = 250;\nconst DEFAULT_MAX_BUFFER_SIZE = 1000;\n\nconst BREAKER_THRESHOLD = 5;\nconst BREAKER_COOLDOWN_MS = 30_000;\n\nexport interface QueueOptions {\n batchMs?: number;\n batchSize?: number;\n /** @internal Test-only. Override the service worker check. */\n isServiceWorkerActive?: () => boolean;\n maxBufferSize?: number;\n transport: HttpTransport;\n}\n\nexport class BatchQueue {\n private readonly buffer: Envelope[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private failures = 0;\n private pausedUntil = 0;\n private readonly batchSize: number;\n private readonly batchMs: number;\n private readonly maxBufferSize: number;\n private readonly transport: HttpTransport;\n private readonly isServiceWorkerActive: () => boolean;\n\n constructor(opts: QueueOptions) {\n this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;\n this.batchMs = opts.batchMs ?? DEFAULT_BATCH_MS;\n this.maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n this.transport = opts.transport;\n this.isServiceWorkerActive = opts.isServiceWorkerActive ?? hasServiceWorker;\n }\n\n start(): void {\n if (this.timer) {\n return;\n }\n\n this.timer = setInterval(() => {\n this.flush();\n }, this.batchMs);\n\n if (typeof globalThis.addEventListener === \"function\") {\n globalThis.addEventListener(\"visibilitychange\", this.onVisibilityChange);\n globalThis.addEventListener(\"beforeunload\", this.onBeforeUnload);\n }\n }\n\n enqueue(envelope: Envelope): void {\n this.buffer.push(envelope);\n\n if (this.buffer.length > this.maxBufferSize) {\n const overflow = this.buffer.length - this.maxBufferSize;\n this.buffer.splice(0, overflow);\n log.warn(\"buffer full, dropped %d oldest envelopes\", overflow);\n }\n\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n flush(): void {\n if (this.flushing || this.buffer.length === 0) {\n return;\n }\n\n if (this.failures >= BREAKER_THRESHOLD && Date.now() < this.pausedUntil) {\n return;\n }\n\n this.flushing = true;\n const batch = this.buffer.splice(0, this.batchSize);\n const meta = {\n retryCount: this.failures,\n queueDepth: this.buffer.length,\n };\n\n this.transport\n .send(batch, meta)\n .then(() => {\n if (this.failures > 0) {\n log.info(\"send recovered after %d failures\", this.failures);\n }\n this.failures = 0;\n })\n .catch(() => {\n if (!this.isServiceWorkerActive()) {\n this.buffer.unshift(...batch);\n }\n this.failures++;\n if (this.failures >= BREAKER_THRESHOLD) {\n this.pausedUntil = Date.now() + BREAKER_COOLDOWN_MS;\n log.warn(\n \"pausing sends for %dms after %d consecutive failures\",\n BREAKER_COOLDOWN_MS,\n this.failures\n );\n }\n })\n .finally(() => {\n this.flushing = false;\n });\n }\n\n dispose(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n\n if (typeof globalThis.removeEventListener === \"function\") {\n globalThis.removeEventListener(\n \"visibilitychange\",\n this.onVisibilityChange\n );\n globalThis.removeEventListener(\"beforeunload\", this.onBeforeUnload);\n }\n\n this.drain();\n }\n\n private drain(): void {\n while (this.buffer.length > 0) {\n const batch = this.buffer.splice(0, this.batchSize);\n this.transport.send(batch).catch(() => {\n /* best-effort — SW BackgroundSync handles persistence */\n });\n }\n }\n\n private readonly onVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") {\n this.drain();\n }\n };\n\n private readonly onBeforeUnload = (): void => {\n this.drain();\n };\n}\n"],"mappings":";;;AAKA,MAAM,MAAM,aAAa,QAAQ;AAEjC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,0BAA0B;AAEhC,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;AAW5B,IAAa,aAAb,MAAwB;CACtB,SAAsC,EAAE;CACxC,QAAuD;CACvD,WAAmB;CACnB,WAAmB;CACnB,cAAsB;CACtB;CACA;CACA;CACA;CACA;CAEA,YAAY,MAAoB;AAC9B,OAAK,YAAY,KAAK,aAAa;AACnC,OAAK,UAAU,KAAK,WAAW;AAC/B,OAAK,gBAAgB,KAAK,iBAAiB;AAC3C,OAAK,YAAY,KAAK;AACtB,OAAK,wBAAwB,KAAK,yBAAyB;;CAG7D,QAAc;AACZ,MAAI,KAAK,MACP;AAGF,OAAK,QAAQ,kBAAkB;AAC7B,QAAK,OAAO;KACX,KAAK,QAAQ;AAEhB,MAAI,OAAO,WAAW,qBAAqB,YAAY;AACrD,cAAW,iBAAiB,oBAAoB,KAAK,mBAAmB;AACxE,cAAW,iBAAiB,gBAAgB,KAAK,eAAe;;;CAIpE,QAAQ,UAA0B;AAChC,OAAK,OAAO,KAAK,SAAS;AAE1B,MAAI,KAAK,OAAO,SAAS,KAAK,eAAe;GAC3C,MAAM,WAAW,KAAK,OAAO,SAAS,KAAK;AAC3C,QAAK,OAAO,OAAO,GAAG,SAAS;AAC/B,OAAI,KAAK,4CAA4C,SAAS;;AAGhE,MAAI,KAAK,OAAO,UAAU,KAAK,UAC7B,MAAK,OAAO;;CAIhB,QAAc;AACZ,MAAI,KAAK,YAAY,KAAK,OAAO,WAAW,EAC1C;AAGF,MAAI,KAAK,YAAY,qBAAqB,KAAK,KAAK,GAAG,KAAK,YAC1D;AAGF,OAAK,WAAW;EAChB,MAAM,QAAQ,KAAK,OAAO,OAAO,GAAG,KAAK,UAAU;EACnD,MAAM,OAAO;GACX,YAAY,KAAK;GACjB,YAAY,KAAK,OAAO;GACzB;AAED,OAAK,UACF,KAAK,OAAO,KAAK,CACjB,WAAW;AACV,OAAI,KAAK,WAAW,EAClB,KAAI,KAAK,oCAAoC,KAAK,SAAS;AAE7D,QAAK,WAAW;IAChB,CACD,YAAY;AACX,OAAI,CAAC,KAAK,uBAAuB,CAC/B,MAAK,OAAO,QAAQ,GAAG,MAAM;AAE/B,QAAK;AACL,OAAI,KAAK,YAAY,mBAAmB;AACtC,SAAK,cAAc,KAAK,KAAK,GAAG;AAChC,QAAI,KACF,wDACA,qBACA,KAAK,SACN;;IAEH,CACD,cAAc;AACb,QAAK,WAAW;IAChB;;CAGN,UAAgB;AACd,MAAI,KAAK,OAAO;AACd,iBAAc,KAAK,MAAM;AACzB,QAAK,QAAQ;;AAGf,MAAI,OAAO,WAAW,wBAAwB,YAAY;AACxD,cAAW,oBACT,oBACA,KAAK,mBACN;AACD,cAAW,oBAAoB,gBAAgB,KAAK,eAAe;;AAGrE,OAAK,OAAO;;CAGd,QAAsB;AACpB,SAAO,KAAK,OAAO,SAAS,GAAG;GAC7B,MAAM,QAAQ,KAAK,OAAO,OAAO,GAAG,KAAK,UAAU;AACnD,QAAK,UAAU,KAAK,MAAM,CAAC,YAAY,GAErC;;;CAIN,2BAAkD;AAChD,MAAI,SAAS,oBAAoB,SAC/B,MAAK,OAAO;;CAIhB,uBAA8C;AAC5C,OAAK,OAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interfere/react",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
|
|
6
6
|
"keywords": [
|
|
@@ -48,15 +48,13 @@
|
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsdown",
|
|
50
50
|
"dev": "tsdown --watch",
|
|
51
|
-
"test
|
|
52
|
-
"
|
|
53
|
-
"typecheck": "tsc --noEmit --incremental",
|
|
54
|
-
"test": "bun run test:unit"
|
|
51
|
+
"test": "vitest run --coverage",
|
|
52
|
+
"typecheck": "tsc --noEmit --incremental"
|
|
55
53
|
},
|
|
56
54
|
"dependencies": {
|
|
57
55
|
"@fingerprintjs/fingerprintjs": "^5.1.0",
|
|
58
|
-
"@interfere/constants": "^8.
|
|
59
|
-
"@interfere/types": "^8.
|
|
56
|
+
"@interfere/constants": "^8.1.0",
|
|
57
|
+
"@interfere/types": "^8.1.0",
|
|
60
58
|
"@ua-parser-js/pro-enterprise": "^2.0.6",
|
|
61
59
|
"rrweb": "2.0.0-alpha.4",
|
|
62
60
|
"uuid": "^13.0.0"
|
|
@@ -66,21 +64,20 @@
|
|
|
66
64
|
"react-dom": ">=19"
|
|
67
65
|
},
|
|
68
66
|
"devDependencies": {
|
|
69
|
-
"@interfere/typescript-config": "^8.
|
|
70
|
-
"@interfere/
|
|
67
|
+
"@interfere/typescript-config": "^8.1.0",
|
|
68
|
+
"@interfere/test-utils": "^1.0.0",
|
|
71
69
|
"@rrweb/types": "2.0.0-alpha.20",
|
|
72
|
-
"@testing-library/react": "^16.3.2",
|
|
73
70
|
"@types/node": "^24.12.0",
|
|
74
71
|
"@types/react": "19.2.14",
|
|
75
72
|
"@types/react-dom": "19.2.3",
|
|
76
73
|
"@vitejs/plugin-react": "^6.0.1",
|
|
77
|
-
"@vitest/browser": "4.1.
|
|
78
|
-
"@vitest/browser-playwright": "4.1.
|
|
79
|
-
"@vitest/coverage-v8": "^4.
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"vitest": "
|
|
74
|
+
"@vitest/browser": "4.1.2",
|
|
75
|
+
"@vitest/browser-playwright": "4.1.2",
|
|
76
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
77
|
+
"playwright": "^1.59.0",
|
|
78
|
+
"tsdown": "^0.21.6",
|
|
79
|
+
"typescript": "6.0.2",
|
|
80
|
+
"vitest": "^4.1.2",
|
|
81
|
+
"vitest-browser-react": "2.1.0"
|
|
85
82
|
}
|
|
86
83
|
}
|