@ianmenethil/zp-observer 6.0.0 → 6.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.
Files changed (107) hide show
  1. package/README.md +19 -276
  2. package/dist/adapters/browser-lifecycle-adapter.cjs +51 -0
  3. package/dist/adapters/browser-lifecycle-adapter.js +48 -0
  4. package/dist/adapters/iframe-detector-adapter.cjs +108 -0
  5. package/dist/adapters/iframe-detector-adapter.js +106 -0
  6. package/dist/client/create-telemetry-client.cjs +136 -0
  7. package/dist/client/create-telemetry-client.js +134 -0
  8. package/dist/client/state-machine.cjs +20 -0
  9. package/dist/client/state-machine.js +18 -0
  10. package/dist/diagnostics/diagnostics-buffer.cjs +34 -0
  11. package/dist/diagnostics/diagnostics-buffer.js +32 -0
  12. package/dist/diagnostics/preflight.cjs +36 -0
  13. package/dist/diagnostics/preflight.js +34 -0
  14. package/dist/events/envelope.cjs +23 -0
  15. package/dist/events/envelope.js +21 -0
  16. package/dist/index.cjs +23 -0
  17. package/dist/index.d.ts +230 -0
  18. package/dist/index.js +10 -0
  19. package/dist/persistence/local-storage-outbox.cjs +56 -0
  20. package/dist/persistence/local-storage-outbox.js +54 -0
  21. package/dist/persistence/memory-outbox.cjs +23 -0
  22. package/dist/persistence/memory-outbox.js +21 -0
  23. package/dist/runtime/event-pipeline.cjs +64 -0
  24. package/dist/runtime/event-pipeline.js +62 -0
  25. package/dist/runtime/heartbeat-scheduler.cjs +46 -0
  26. package/dist/runtime/heartbeat-scheduler.js +44 -0
  27. package/dist/runtime/session-manager.cjs +47 -0
  28. package/dist/runtime/session-manager.js +45 -0
  29. package/dist/transport/beacon.cjs +14 -0
  30. package/dist/transport/beacon.js +12 -0
  31. package/dist/transport/callback-transport.cjs +19 -0
  32. package/dist/transport/callback-transport.js +17 -0
  33. package/dist/transport/http-transport.cjs +62 -0
  34. package/dist/transport/http-transport.js +60 -0
  35. package/dist/types/internal.cjs +3 -0
  36. package/dist/types/internal.js +0 -0
  37. package/dist/types/public.cjs +3 -0
  38. package/dist/types/public.js +0 -0
  39. package/dist/utils/ids.cjs +20 -0
  40. package/dist/utils/ids.js +17 -0
  41. package/dist/utils/safe-globals.cjs +11 -0
  42. package/dist/utils/safe-globals.js +9 -0
  43. package/dist/version.cjs +5 -0
  44. package/dist/version.js +2 -0
  45. package/package.json +29 -89
  46. package/PRIVACY.md +0 -67
  47. package/dist/auto-patch.cjs +0 -171
  48. package/dist/auto-patch.cjs.map +0 -7
  49. package/dist/auto-patch.mjs +0 -148
  50. package/dist/auto-patch.mjs.map +0 -7
  51. package/dist/session.cjs +0 -1186
  52. package/dist/session.cjs.map +0 -7
  53. package/dist/session.mjs +0 -1163
  54. package/dist/session.mjs.map +0 -7
  55. package/dist/types/auto-patch.d.ts +0 -9
  56. package/dist/types/auto-patch.d.ts.map +0 -1
  57. package/dist/types/core/beacon.d.ts +0 -6
  58. package/dist/types/core/beacon.d.ts.map +0 -1
  59. package/dist/types/core/detection.d.ts +0 -34
  60. package/dist/types/core/detection.d.ts.map +0 -1
  61. package/dist/types/core/event-bus.d.ts +0 -21
  62. package/dist/types/core/event-bus.d.ts.map +0 -1
  63. package/dist/types/core/experimental.d.ts +0 -35
  64. package/dist/types/core/experimental.d.ts.map +0 -1
  65. package/dist/types/core/heartbeat.d.ts +0 -32
  66. package/dist/types/core/heartbeat.d.ts.map +0 -1
  67. package/dist/types/core/lifecycle.d.ts +0 -31
  68. package/dist/types/core/lifecycle.d.ts.map +0 -1
  69. package/dist/types/core/observer.d.ts +0 -10
  70. package/dist/types/core/observer.d.ts.map +0 -1
  71. package/dist/types/core/outbox.d.ts +0 -20
  72. package/dist/types/core/outbox.d.ts.map +0 -1
  73. package/dist/types/core/random.d.ts +0 -8
  74. package/dist/types/core/random.d.ts.map +0 -1
  75. package/dist/types/core/shortcode.d.ts +0 -17
  76. package/dist/types/core/shortcode.d.ts.map +0 -1
  77. package/dist/types/core/types.d.ts +0 -292
  78. package/dist/types/core/types.d.ts.map +0 -1
  79. package/dist/types/diagnostics/preflight.d.ts +0 -17
  80. package/dist/types/diagnostics/preflight.d.ts.map +0 -1
  81. package/dist/types/index.d.ts +0 -41
  82. package/dist/types/index.d.ts.map +0 -1
  83. package/dist/types/integration/devicefp-bridge.d.ts +0 -31
  84. package/dist/types/integration/devicefp-bridge.d.ts.map +0 -1
  85. package/dist/types/integration/hpp-bridge.d.ts +0 -13
  86. package/dist/types/integration/hpp-bridge.d.ts.map +0 -1
  87. package/dist/types/integration/zenpay-auto-patch.d.ts +0 -28
  88. package/dist/types/integration/zenpay-auto-patch.d.ts.map +0 -1
  89. package/dist/types/outcome.d.ts +0 -20
  90. package/dist/types/outcome.d.ts.map +0 -1
  91. package/dist/types/session.d.ts +0 -54
  92. package/dist/types/session.d.ts.map +0 -1
  93. package/dist/types/transport/callback-transport.d.ts +0 -17
  94. package/dist/types/transport/callback-transport.d.ts.map +0 -1
  95. package/dist/types/transport/http-transport.d.ts +0 -30
  96. package/dist/types/transport/http-transport.d.ts.map +0 -1
  97. package/dist/types/umd.d.ts +0 -16
  98. package/dist/types/umd.d.ts.map +0 -1
  99. package/dist/zp-observer.cjs +0 -1375
  100. package/dist/zp-observer.cjs.map +0 -7
  101. package/dist/zp-observer.js +0 -1377
  102. package/dist/zp-observer.js.map +0 -7
  103. package/dist/zp-observer.min.js +0 -2
  104. package/dist/zp-observer.min.js.map +0 -7
  105. package/dist/zp-observer.min.obf.js +0 -1
  106. package/dist/zp-observer.mjs +0 -1352
  107. package/dist/zp-observer.mjs.map +0 -7
@@ -0,0 +1,45 @@
1
+ import { assertTransition } from "../client/state-machine.js";
2
+ import { generateCorrelationId, generateEventId } from "../utils/ids.js";
3
+ export function createSessionManager(options) {
4
+ const now = options.now ?? Date.now;
5
+ const sessionId = options.sessionId;
6
+ const correlationId = options.correlationId ?? generateCorrelationId();
7
+ let state = "idle";
8
+ let sequence = 0;
9
+ let startedAt = null;
10
+ let stoppedAt = null;
11
+ return {
12
+ sessionId,
13
+ correlationId,
14
+ nextSequence() {
15
+ sequence += 1;
16
+ return sequence;
17
+ },
18
+ nextEventId() {
19
+ return generateEventId(sequence);
20
+ },
21
+ transition(next) {
22
+ assertTransition(state, next);
23
+ state = next;
24
+ if (next === "starting" && startedAt === null) {
25
+ startedAt = now();
26
+ }
27
+ if (next === "closed") {
28
+ stoppedAt = now();
29
+ }
30
+ },
31
+ snapshot() {
32
+ const currentNow = now();
33
+ const elapsedMs = startedAt === null ? 0 : (stoppedAt ?? currentNow) - startedAt;
34
+ return {
35
+ state,
36
+ sessionId,
37
+ correlationId,
38
+ startedAt,
39
+ stoppedAt,
40
+ eventCount: sequence,
41
+ elapsedMs
42
+ };
43
+ }
44
+ };
45
+ }
@@ -0,0 +1,14 @@
1
+ const JSON_CONTENT_TYPE = "application/json";
2
+ function postJsonBeacon(url, body) {
3
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") {
4
+ return false;
5
+ }
6
+ try {
7
+ const blob = new Blob([JSON.stringify(body)], { type: JSON_CONTENT_TYPE });
8
+ return navigator.sendBeacon(url, blob);
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ exports.postJsonBeacon = postJsonBeacon;
@@ -0,0 +1,12 @@
1
+ const JSON_CONTENT_TYPE = "application/json";
2
+ export function postJsonBeacon(url, body) {
3
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") {
4
+ return false;
5
+ }
6
+ try {
7
+ const blob = new Blob([JSON.stringify(body)], { type: JSON_CONTENT_TYPE });
8
+ return navigator.sendBeacon(url, blob);
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
@@ -0,0 +1,19 @@
1
+ function callbackTransport(options = {}) {
2
+ return {
3
+ async send(envelope) {
4
+ if (!options.onSend) {
5
+ return { ok: true };
6
+ }
7
+ try {
8
+ return await options.onSend(envelope) ?? { ok: true };
9
+ } catch (error) {
10
+ return {
11
+ ok: false,
12
+ error: error instanceof Error ? error.message : String(error)
13
+ };
14
+ }
15
+ }
16
+ };
17
+ }
18
+
19
+ exports.callbackTransport = callbackTransport;
@@ -0,0 +1,17 @@
1
+ export function callbackTransport(options = {}) {
2
+ return {
3
+ async send(envelope) {
4
+ if (!options.onSend) {
5
+ return { ok: true };
6
+ }
7
+ try {
8
+ return await options.onSend(envelope) ?? { ok: true };
9
+ } catch (error) {
10
+ return {
11
+ ok: false,
12
+ error: error instanceof Error ? error.message : String(error)
13
+ };
14
+ }
15
+ }
16
+ };
17
+ }
@@ -0,0 +1,62 @@
1
+ const {postJsonBeacon} = require('./beacon.cjs');
2
+ function resolveHeaders(headers) {
3
+ if (!headers) {
4
+ return {};
5
+ }
6
+ return typeof headers === "function" ? headers() : headers;
7
+ }
8
+ function httpTransport(options) {
9
+ if (!options?.endpoint) {
10
+ throw new Error("httpTransport requires endpoint");
11
+ }
12
+ const fetchImpl = options.fetch ?? (typeof fetch === "undefined" ? undefined : fetch.bind(globalThis));
13
+ const timeoutMs = options.timeoutMs ?? 8000;
14
+ const credentials = options.credentials ?? "include";
15
+ return {
16
+ async send(envelope) {
17
+ if (!fetchImpl) {
18
+ return {
19
+ ok: false,
20
+ error: "Fetch is not available in this runtime."
21
+ };
22
+ }
23
+ if (envelope.kind !== "transport.heartbeat" && postJsonBeacon(options.endpoint, envelope)) {
24
+ return {
25
+ ok: true
26
+ };
27
+ }
28
+ const controller = typeof AbortController === "undefined" ? undefined : new AbortController;
29
+ const timer = controller === undefined ? undefined : setTimeout(() => {
30
+ controller.abort();
31
+ }, timeoutMs);
32
+ try {
33
+ const response = await fetchImpl(options.endpoint, {
34
+ method: "POST",
35
+ headers: {
36
+ "content-type": "application/json",
37
+ ...resolveHeaders(options.headers)
38
+ },
39
+ body: JSON.stringify(envelope),
40
+ credentials,
41
+ ...controller ? { signal: controller.signal } : {}
42
+ });
43
+ return {
44
+ ok: response.ok,
45
+ status: response.status,
46
+ ...response.ok ? {} : { error: `HTTP ${response.status}` }
47
+ };
48
+ } catch (error) {
49
+ return {
50
+ ok: false,
51
+ error: error instanceof Error ? error.message : String(error)
52
+ };
53
+ } finally {
54
+ if (timer !== undefined) {
55
+ clearTimeout(timer);
56
+ }
57
+ }
58
+ }
59
+ };
60
+ }
61
+
62
+ exports.httpTransport = httpTransport;
@@ -0,0 +1,60 @@
1
+ import { postJsonBeacon } from "./beacon.js";
2
+ function resolveHeaders(headers) {
3
+ if (!headers) {
4
+ return {};
5
+ }
6
+ return typeof headers === "function" ? headers() : headers;
7
+ }
8
+ export function httpTransport(options) {
9
+ if (!options?.endpoint) {
10
+ throw new Error("httpTransport requires endpoint");
11
+ }
12
+ const fetchImpl = options.fetch ?? (typeof fetch === "undefined" ? undefined : fetch.bind(globalThis));
13
+ const timeoutMs = options.timeoutMs ?? 8000;
14
+ const credentials = options.credentials ?? "include";
15
+ return {
16
+ async send(envelope) {
17
+ if (!fetchImpl) {
18
+ return {
19
+ ok: false,
20
+ error: "Fetch is not available in this runtime."
21
+ };
22
+ }
23
+ if (envelope.kind !== "transport.heartbeat" && postJsonBeacon(options.endpoint, envelope)) {
24
+ return {
25
+ ok: true
26
+ };
27
+ }
28
+ const controller = typeof AbortController === "undefined" ? undefined : new AbortController;
29
+ const timer = controller === undefined ? undefined : setTimeout(() => {
30
+ controller.abort();
31
+ }, timeoutMs);
32
+ try {
33
+ const response = await fetchImpl(options.endpoint, {
34
+ method: "POST",
35
+ headers: {
36
+ "content-type": "application/json",
37
+ ...resolveHeaders(options.headers)
38
+ },
39
+ body: JSON.stringify(envelope),
40
+ credentials,
41
+ ...controller ? { signal: controller.signal } : {}
42
+ });
43
+ return {
44
+ ok: response.ok,
45
+ status: response.status,
46
+ ...response.ok ? {} : { error: `HTTP ${response.status}` }
47
+ };
48
+ } catch (error) {
49
+ return {
50
+ ok: false,
51
+ error: error instanceof Error ? error.message : String(error)
52
+ };
53
+ } finally {
54
+ if (timer !== undefined) {
55
+ clearTimeout(timer);
56
+ }
57
+ }
58
+ }
59
+ };
60
+ }
@@ -0,0 +1,3 @@
1
+
2
+
3
+
File without changes
@@ -0,0 +1,3 @@
1
+
2
+
3
+
File without changes
@@ -0,0 +1,20 @@
1
+ function randomChunk() {
2
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
3
+ const buffer = new Uint32Array(2);
4
+ crypto.getRandomValues(buffer);
5
+ return `${buffer[0].toString(36)}${buffer[1].toString(36)}`;
6
+ }
7
+ return `${Math.floor(Math.random() * 4294967295).toString(36)}${Math.floor(Math.random() * 4294967295).toString(36)}`;
8
+ }
9
+ function generateCorrelationId() {
10
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
11
+ return crypto.randomUUID();
12
+ }
13
+ return `cid_${Date.now().toString(36)}_${randomChunk()}`;
14
+ }
15
+ function generateEventId(sequence) {
16
+ return `evt_${sequence.toString(36)}_${randomChunk()}`;
17
+ }
18
+
19
+ exports.generateCorrelationId = generateCorrelationId;
20
+ exports.generateEventId = generateEventId;
@@ -0,0 +1,17 @@
1
+ function randomChunk() {
2
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
3
+ const buffer = new Uint32Array(2);
4
+ crypto.getRandomValues(buffer);
5
+ return `${buffer[0].toString(36)}${buffer[1].toString(36)}`;
6
+ }
7
+ return `${Math.floor(Math.random() * 4294967295).toString(36)}${Math.floor(Math.random() * 4294967295).toString(36)}`;
8
+ }
9
+ export function generateCorrelationId() {
10
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
11
+ return crypto.randomUUID();
12
+ }
13
+ return `cid_${Date.now().toString(36)}_${randomChunk()}`;
14
+ }
15
+ export function generateEventId(sequence) {
16
+ return `evt_${sequence.toString(36)}_${randomChunk()}`;
17
+ }
@@ -0,0 +1,11 @@
1
+ function readDefaultContext() {
2
+ return {
3
+ pageUrl: typeof location === "undefined" ? undefined : location.href,
4
+ referrer: typeof document === "undefined" ? undefined : document.referrer,
5
+ userAgent: typeof navigator === "undefined" ? undefined : navigator.userAgent,
6
+ visibilityState: typeof document === "undefined" ? undefined : document.visibilityState,
7
+ online: typeof navigator === "undefined" ? null : navigator.onLine
8
+ };
9
+ }
10
+
11
+ exports.readDefaultContext = readDefaultContext;
@@ -0,0 +1,9 @@
1
+ export function readDefaultContext() {
2
+ return {
3
+ pageUrl: typeof location === "undefined" ? undefined : location.href,
4
+ referrer: typeof document === "undefined" ? undefined : document.referrer,
5
+ userAgent: typeof navigator === "undefined" ? undefined : navigator.userAgent,
6
+ visibilityState: typeof document === "undefined" ? undefined : document.visibilityState,
7
+ online: typeof navigator === "undefined" ? null : navigator.onLine
8
+ };
9
+ }
@@ -0,0 +1,5 @@
1
+ const VERSION = "0.1.0";
2
+ const SCHEMA_VERSION = 1;
3
+
4
+ exports.VERSION = VERSION;
5
+ exports.SCHEMA_VERSION = SCHEMA_VERSION;
@@ -0,0 +1,2 @@
1
+ export const VERSION = "0.1.0";
2
+ export const SCHEMA_VERSION = 1;
package/package.json CHANGED
@@ -1,91 +1,31 @@
1
1
  {
2
- "name": "@ianmenethil/zp-observer",
3
- "version": "6.0.0",
4
- "description": "Telemetry bridge for Zenith Payments hosted checkout. Tracks modal open, close, tab dismissal, idle, and network abandonment.",
5
- "license": "MIT",
6
- "author": "Zenith Payments",
7
- "homepage": "https://www.zenithpayments.com.au",
8
- "type": "module",
9
- "main": "./dist/zp-observer.min.js",
10
- "types": "./dist/types/index.d.ts",
11
- "exports": {
12
- ".": {
13
- "types": "./dist/types/index.d.ts",
14
- "import": "./dist/zp-observer.mjs",
15
- "require": "./dist/zp-observer.cjs",
16
- "default": "./dist/zp-observer.min.js"
17
- },
18
- "./auto-patch": {
19
- "types": "./dist/types/auto-patch.d.ts",
20
- "import": "./dist/auto-patch.mjs",
21
- "require": "./dist/auto-patch.cjs"
22
- },
23
- "./session": {
24
- "types": "./dist/types/session.d.ts",
25
- "import": "./dist/session.mjs",
26
- "require": "./dist/session.cjs"
27
- }
28
- },
29
- "unpkg": "./dist/zp-observer.min.js",
30
- "jsdelivr": "./dist/zp-observer.min.js",
31
- "files": [
32
- "dist/zp-observer.js",
33
- "dist/zp-observer.min.js",
34
- "dist/zp-observer.min.obf.js",
35
- "dist/zp-observer.mjs",
36
- "dist/zp-observer.cjs",
37
- "dist/auto-patch.mjs",
38
- "dist/auto-patch.cjs",
39
- "dist/session.mjs",
40
- "dist/session.cjs",
41
- "dist/*.map",
42
- "dist/types/",
43
- "README.md",
44
- "PRIVACY.md",
45
- "LICENSE"
46
- ],
47
- "engines": {
48
- "node": ">=22"
49
- },
50
- "scripts": {
51
- "build": "npm run check && bun run scripts/build.ts && npm run build:types",
52
- "build:types": "tsc -p tsconfig.build.json",
53
- "build:local": "npm run build && bun run scripts/deploy-local.ts",
54
- "typecheck": "tsc -p tsconfig.json --noEmit",
55
- "lint": "eslint -c eslint.config.js .",
56
- "lint:fix": "eslint -c eslint.config.js . --fix",
57
- "knip": "knip --config knip.json",
58
- "jscpd": "jscpd src/ test/",
59
- "check": "npm run typecheck && npm run lint && npm run knip && npm run jscpd && npm run test",
60
- "prepack": "npm run build",
61
- "publish:npm": "npm publish --access public",
62
- "test": "vitest run",
63
- "test:watch": "vitest"
64
- },
65
- "devDependencies": {
66
- "@eslint/js": "^9.18.0",
67
- "@types/jsdom": "^21.1.7",
68
- "@types/node": "^22.10.0",
69
- "esbuild": "^0.25.0",
70
- "eslint": "^9.18.0",
71
- "javascript-obfuscator": "^4.1.1",
72
- "jscpd": "^4.0.9",
73
- "jsdom": "^25.0.1",
74
- "knip": "^6.6.0",
75
- "typescript": "^5.7.0",
76
- "typescript-eslint": "^8.21.0",
77
- "vitest": "^2.1.0"
78
- },
79
- "keywords": [
80
- "zenith-payments",
81
- "zenpay",
82
- "telemetry",
83
- "observer",
84
- "payments",
85
- "hosted-checkout",
86
- "iframe"
87
- ],
88
- "publishConfig": {
89
- "access": "public"
90
- }
2
+ "name": "@ianmenethil/zp-observer",
3
+ "version": "6.1.0",
4
+ "description": "Telemetry runtime for Zenith hosted checkout session lifecycle, fingerprint, HPP, heartbeat, and outcome events.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "bun run ./scripts/build.mjs",
23
+ "test": "bun test",
24
+ "check": "bun test && bun run ./scripts/build.mjs",
25
+ "prepublishOnly": "npm run check"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "license": "MIT"
91
31
  }
package/PRIVACY.md DELETED
@@ -1,67 +0,0 @@
1
- # Privacy notice — @ianmenethil/zp-observer
2
-
3
- This library collects a small, fixed set of fields for each payment session. It does **not** read form values, iframe contents, cookies, or arbitrary DOM state.
4
-
5
- ## Fields collected
6
-
7
- ### `open` event (once per session)
8
-
9
- | Field | Purpose |
10
- |---|---|
11
- | `sessionId` | Identifier you pass in; typically the merchant unique payment id |
12
- | `timestamp` | When the modal iframe was first detected |
13
- | `iframeSrc` | The iframe's `src` URL (Zenith-hosted origin) |
14
- | `userAgent` | `navigator.userAgent` — for debugging browser-specific issues |
15
- | `screenWidth` / `screenHeight` | Screen size — for mobile vs desktop bucketing |
16
- | `navigationType` | `performance.getEntriesByType('navigation')[0].type` |
17
- | `metadata` | Whatever you pass into `createObserver({ metadata: ... })` |
18
-
19
- ### `heartbeat` event (every `heartbeatMs`, default 5s)
20
-
21
- | Field | Purpose |
22
- |---|---|
23
- | `sessionId`, `timestamp`, `metadata` | as above |
24
- | `sequence` | Monotonic counter for deduplication |
25
- | `missedBeats` | How many consecutive failures we've seen |
26
-
27
- ### `close` event (once per session)
28
-
29
- | Field | Purpose |
30
- |---|---|
31
- | `sessionId`, `timestamp`, `metadata` | as above |
32
- | `reason` | One of seven documented reasons (see README) |
33
- | `elapsedMs` | How long the session lasted |
34
- | `heartbeatCount` | Total heartbeats sent |
35
- | `missedBeats` | Consecutive failures at close time |
36
- | `wasPersisted` | `pagehide.persisted` — true if page went into bfcache |
37
-
38
- ## Fields **not** collected
39
-
40
- - No form field values or payment card data (cross-origin blocks this anyway)
41
- - No cookies or storage contents
42
- - No click / keystroke events
43
- - No page URL beyond what you pass in `metadata`
44
- - No IP address (your server records that when it receives the request)
45
-
46
- ## Scrubbing
47
-
48
- Use `beforeSend` to mutate or drop events before they leave the browser:
49
-
50
- ```ts
51
- createObserver({
52
- sessionId: mUPID,
53
- transport: httpTransport({ ... }),
54
- beforeSend: (ev) => {
55
- if (ev.kind === 'open') {
56
- return { ...ev, userAgent: '' };
57
- }
58
- return ev;
59
- },
60
- });
61
- ```
62
-
63
- Return `null` to drop an event entirely.
64
-
65
- ## Cross-origin note
66
-
67
- The ZenPay payment iframe is on a Zenith-controlled domain. Browser same-origin policy means this library **cannot** read anything inside the iframe — no form values, no DOM, no keystrokes. It only observes the iframe element itself (presence, size, visibility) from the parent page.