@joryio/web-sdk 0.0.0-stage → 1.3.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 (76) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +54 -0
  3. package/README.md +497 -2
  4. package/dist/core/config.d.ts +101 -0
  5. package/dist/core/config.d.ts.map +1 -0
  6. package/dist/core/device.d.ts +14 -0
  7. package/dist/core/device.d.ts.map +1 -0
  8. package/dist/core/ecommerce.d.ts +67 -0
  9. package/dist/core/ecommerce.d.ts.map +1 -0
  10. package/dist/core/identity.d.ts +36 -0
  11. package/dist/core/identity.d.ts.map +1 -0
  12. package/dist/core/inapp.d.ts +169 -0
  13. package/dist/core/inapp.d.ts.map +1 -0
  14. package/dist/core/network.d.ts +158 -0
  15. package/dist/core/network.d.ts.map +1 -0
  16. package/dist/core/outbox.d.ts +79 -0
  17. package/dist/core/outbox.d.ts.map +1 -0
  18. package/dist/core/pending-attributes.d.ts +43 -0
  19. package/dist/core/pending-attributes.d.ts.map +1 -0
  20. package/dist/core/push.d.ts +59 -0
  21. package/dist/core/push.d.ts.map +1 -0
  22. package/dist/core/queue.d.ts +107 -0
  23. package/dist/core/queue.d.ts.map +1 -0
  24. package/dist/core/sdk.d.ts +169 -0
  25. package/dist/core/sdk.d.ts.map +1 -0
  26. package/dist/core/session.d.ts +36 -0
  27. package/dist/core/session.d.ts.map +1 -0
  28. package/dist/core/storage.d.ts +17 -0
  29. package/dist/core/storage.d.ts.map +1 -0
  30. package/dist/core/user.d.ts +28 -0
  31. package/dist/core/user.d.ts.map +1 -0
  32. package/dist/core/utm.d.ts +39 -0
  33. package/dist/core/utm.d.ts.map +1 -0
  34. package/dist/core/values.d.ts +3 -0
  35. package/dist/core/values.d.ts.map +1 -0
  36. package/dist/core/write-retry.d.ts +66 -0
  37. package/dist/core/write-retry.d.ts.map +1 -0
  38. package/dist/index.d.mts +711 -0
  39. package/dist/index.d.ts +21 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/joryio-sw.js +6 -0
  42. package/dist/joryio.cjs.js +7265 -0
  43. package/dist/joryio.min.js +7 -0
  44. package/dist/joryio.min.js.map +1 -0
  45. package/dist/joryio.mjs +7254 -0
  46. package/dist/joryio.umd.js +7271 -0
  47. package/dist/utils/bucket-hash.d.ts +2 -0
  48. package/dist/utils/bucket-hash.d.ts.map +1 -0
  49. package/dist/utils/logger.d.ts +14 -0
  50. package/dist/utils/logger.d.ts.map +1 -0
  51. package/dist/utils/uuid.d.ts +3 -0
  52. package/dist/utils/uuid.d.ts.map +1 -0
  53. package/package.json +90 -4
  54. package/src/core/config.ts +362 -0
  55. package/src/core/device.ts +50 -0
  56. package/src/core/ecommerce.ts +482 -0
  57. package/src/core/identity.ts +251 -0
  58. package/src/core/inapp.ts +2419 -0
  59. package/src/core/network.ts +1261 -0
  60. package/src/core/outbox.ts +707 -0
  61. package/src/core/pending-attributes.ts +350 -0
  62. package/src/core/push.ts +667 -0
  63. package/src/core/queue.ts +894 -0
  64. package/src/core/sdk.ts +3108 -0
  65. package/src/core/session.ts +311 -0
  66. package/src/core/storage.ts +157 -0
  67. package/src/core/user.ts +203 -0
  68. package/src/core/utm.ts +304 -0
  69. package/src/core/values.ts +85 -0
  70. package/src/core/write-retry.ts +136 -0
  71. package/src/index.ts +305 -0
  72. package/src/snippet/loader.js +134 -0
  73. package/src/utils/bucket-hash.ts +44 -0
  74. package/src/utils/logger.ts +58 -0
  75. package/src/utils/uuid.ts +29 -0
  76. package/src/worker/joryio-sw.js +218 -0
@@ -0,0 +1,2 @@
1
+ export declare function bucketHash(input: string): number;
2
+ //# sourceMappingURL=bucket-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bucket-hash.d.ts","sourceRoot":"","sources":["../../src/utils/bucket-hash.ts"],"names":[],"mappings":"AA8BA,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAahD"}
@@ -0,0 +1,14 @@
1
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
2
+ export declare class Logger {
3
+ private enabled;
4
+ private level;
5
+ constructor(enabled: boolean, level?: LogLevel);
6
+ private shouldLog;
7
+ error(...args: any[]): void;
8
+ warn(...args: any[]): void;
9
+ info(...args: any[]): void;
10
+ debug(...args: any[]): void;
11
+ setEnabled(enabled: boolean): void;
12
+ setLevel(level: LogLevel): void;
13
+ }
14
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAS3D,qBAAa,MAAM;IACjB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,KAAK,CAAW;gBAEZ,OAAO,EAAE,OAAO,EAAE,KAAK,GAAE,QAAkB;IAKvD,OAAO,CAAC,SAAS;IAIjB,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAM3B,IAAI,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAM1B,IAAI,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAM1B,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAM3B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIlC,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;CAGhC"}
@@ -0,0 +1,3 @@
1
+ export declare function generateUUID(): string;
2
+ export declare function generateShortId(): string;
3
+ //# sourceMappingURL=uuid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../../src/utils/uuid.ts"],"names":[],"mappings":"AAIA,wBAAgB,YAAY,IAAI,MAAM,CAYrC;AAKD,wBAAgB,eAAe,IAAI,MAAM,CAOxC"}
package/package.json CHANGED
@@ -1,6 +1,92 @@
1
1
  {
2
2
  "name": "@joryio/web-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.3.0",
4
+ "description": "Joryio Web Tracking SDK - Lightweight analytics and event tracking",
5
+ "main": "dist/joryio.cjs.js",
6
+ "module": "dist/joryio.mjs",
7
+ "unpkg": "dist/joryio.min.js",
8
+ "jsdelivr": "dist/joryio.min.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "module": {
13
+ "types": "./dist/index.d.mts",
14
+ "default": "./dist/joryio.mjs"
15
+ },
16
+ "import": {
17
+ "types": "./dist/index.d.mts",
18
+ "default": "./dist/joryio.mjs"
19
+ },
20
+ "require": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/joryio.cjs.js"
23
+ }
24
+ },
25
+ "./dist/*": "./dist/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "src",
31
+ "!src/**/*.spec.ts",
32
+ "!src/core/test-harness.ts",
33
+ "CHANGELOG.md"
34
+ ],
35
+ "scripts": {
36
+ "build": "rollup -c",
37
+ "build:watch": "rollup -c -w",
38
+ "dev": "rollup -c -w",
39
+ "type-check": "tsc --noEmit",
40
+ "test": "jest",
41
+ "test:watch": "jest --watch",
42
+ "lint": "eslint src --ext .ts",
43
+ "clean": "rm -rf dist",
44
+ "prepack": "npm run clean && npm run build"
45
+ },
46
+ "keywords": [
47
+ "analytics",
48
+ "tracking",
49
+ "events",
50
+ "marketing-automation",
51
+ "joryio"
52
+ ],
53
+ "author": "Joryio",
54
+ "license": "SEE LICENSE IN LICENSE",
55
+ "devDependencies": {
56
+ "@babel/core": "^7.28.5",
57
+ "@babel/preset-env": "^7.28.5",
58
+ "@babel/preset-typescript": "^7.28.5",
59
+ "@rollup/plugin-commonjs": "^25.0.7",
60
+ "@rollup/plugin-node-resolve": "^15.2.3",
61
+ "@rollup/plugin-terser": "^0.4.4",
62
+ "@rollup/plugin-typescript": "^11.1.5",
63
+ "@types/jest": "^29.5.11",
64
+ "@types/node": "^20.10.6",
65
+ "@typescript-eslint/eslint-plugin": "^6.17.0",
66
+ "@typescript-eslint/parser": "^6.17.0",
67
+ "babel-jest": "^29.7.0",
68
+ "eslint": "^8.56.0",
69
+ "jest": "^29.7.0",
70
+ "rollup": "^4.9.2",
71
+ "rollup-plugin-dts": "^6.1.0",
72
+ "rollup-plugin-visualizer": "^5.12.0",
73
+ "tslib": "^2.6.2",
74
+ "typescript": "^5.3.3"
75
+ },
76
+ "homepage": "https://docs.joryio.com/developers/sdk/web-sdk",
77
+ "repository": {
78
+ "type": "git",
79
+ "url": "https://github.com/joryio/joryio-web-sdk.git"
80
+ },
81
+ "bugs": {
82
+ "url": "https://github.com/joryio/joryio-web-sdk/issues",
83
+ "email": "support@joryio.com"
84
+ },
85
+ "jest": {
86
+ "testEnvironment": "node",
87
+ "testPathIgnorePatterns": [
88
+ "/node_modules/",
89
+ "/dist/"
90
+ ]
91
+ }
92
+ }
@@ -0,0 +1,362 @@
1
+ /**
2
+ * SDK Configuration Types and Defaults
3
+ */
4
+
5
+ export const SDK_VERSION = '1.3.0'; // keep in sync with package.json — the backend serves this in X-SDK-Version headers
6
+ export const DEFAULT_API_ENDPOINT = 'https://api-eu1.joryio.com/api';
7
+
8
+ /**
9
+ * The API base every request is built on (`<base>/v1/track/batch`).
10
+ *
11
+ * Accepts what customers actually write: a bare host ("api-eu1.joryio.com"),
12
+ * a host with scheme, or the full base with "/api". The backend mounts the
13
+ * SDK routes under its global "api" prefix, so a base with NO path gets
14
+ * "/api" appended - the bare-host form used to produce
15
+ * `https://api-eu1.joryio.com/v1/track/batch`, a 404 on production, and the
16
+ * built-in default was that very form (SDK audit, 2026-09-26). The same rule
17
+ * lives in the iOS and Android SDKs; keep the three identical.
18
+ */
19
+ export function normalizeApiEndpoint(raw: string | undefined | null): string {
20
+ const trimmed = (raw || '').trim();
21
+ if (!trimmed) return DEFAULT_API_ENDPOINT;
22
+ const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
23
+ const noSlash = withScheme.replace(/\/+$/, '');
24
+ try {
25
+ const url = new URL(noSlash);
26
+ if (url.pathname === '' || url.pathname === '/') return `${noSlash}/api`;
27
+ } catch {
28
+ // Not a parseable URL: hand it back untouched and let the request fail loudly.
29
+ }
30
+ return noSlash;
31
+ }
32
+ export const DEFAULT_BATCH_SIZE = 50;
33
+ export const DEFAULT_FLUSH_INTERVAL = 5000; // 5 seconds
34
+ /**
35
+ * The shortest `batchFlushInterval` honoured. A smaller value (a typo such as
36
+ * `batchFlushInterval: 5` meaning seconds) would poll the API every few
37
+ * milliseconds; it is raised to this instead (contract item 35).
38
+ */
39
+ export const MIN_FLUSH_INTERVAL = 1000;
40
+ export const DEFAULT_MAX_QUEUE_SIZE = 1000;
41
+ export const DEFAULT_MAX_RETRIES = 3;
42
+ export const DEFAULT_RETRY_BACKOFF_MS = 1000;
43
+ export const DEFAULT_REQUEST_TIMEOUT = 10000; // 10 seconds
44
+ /** In-app periodic re-sync is off unless `inApp.syncInterval` is set; when set, never faster than this. */
45
+ export const MIN_INAPP_SYNC_INTERVAL = 30000; // 30 seconds
46
+ export const DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
47
+
48
+ export interface InAppDisplayOptions {
49
+ /** Where a `custom`-type message sits (other types have their own placement). Default `'center'`. */
50
+ defaultPosition?: 'top' | 'center' | 'bottom';
51
+ /** How messages enter. Default `'fade'`; skipped under `prefers-reduced-motion`. */
52
+ defaultAnimation?: 'fade' | 'slide' | 'none';
53
+ /** Colour of the dim layer behind modal and full-screen messages. Default `'rgba(0, 0, 0, 0.5)'`. */
54
+ backdropColor?: string;
55
+ /** Colour of the close button. Default: the message's own text colour. */
56
+ closeButtonColor?: string;
57
+ /** Stacking order of messages (the backdrop sits one below). Default `999999`. */
58
+ zIndex?: number;
59
+ }
60
+
61
+ export interface InAppOptions {
62
+ // Enable/disable in-app messaging
63
+ enabled?: boolean;
64
+
65
+ /**
66
+ * When the SDK asks the server which campaigns this visitor may see.
67
+ *
68
+ * - `differential-sync` (default): on init, on identify()/reset(), on a new
69
+ * session, when a watched attribute changes, and every `syncInterval` ms
70
+ * if that is set.
71
+ * - `session-start-only`: on init, on identify()/reset() and on a new
72
+ * session only - no attribute-change or periodic re-sync.
73
+ */
74
+ evaluationMode?: 'differential-sync' | 'session-start-only';
75
+
76
+ /**
77
+ * Periodic re-sync in `differential-sync` mode, in milliseconds. Off (0) by
78
+ * default; any value below 30000 is raised to 30000. Paused while the tab is
79
+ * hidden.
80
+ */
81
+ syncInterval?: number;
82
+
83
+ // Display customization
84
+ displayOptions?: InAppDisplayOptions;
85
+
86
+ /**
87
+ * Allow HTML in-app messages. OFF by default.
88
+ *
89
+ * What actually happens, because the name alone cannot carry it:
90
+ *
91
+ * - The server strips `<script>` and every `on*` handler from the authored
92
+ * HTML before it is sent, so author JavaScript does NOT run in normal
93
+ * operation.
94
+ * - The message is still rendered in an iframe with `allow-scripts
95
+ * allow-same-origin`, because this SDK's own bootstrap runs there (height
96
+ * reporting, click logging, the host bridge). That frame therefore CAN
97
+ * execute script, at your page's origin.
98
+ * - So the residual risk is a sanitiser bypass: if one were ever found,
99
+ * injected script would run with your origin's privileges. That is why
100
+ * this is opt-in and why the name keeps `Js` - `allowHtmlInAppMessages`
101
+ * alone would read like a formatting preference.
102
+ *
103
+ * Leaving it off does not disable in-app messaging: NATIVE messages still
104
+ * display, because they are data rendered into DOM text nodes with no
105
+ * interpreter involved. Only HTML campaigns are skipped - and they are
106
+ * skipped without consuming their frequency cap or reporting an impression.
107
+ */
108
+ allowHtmlJsInAppMessages?: boolean;
109
+
110
+ // Callback hooks
111
+ onMessageDisplay?: (message: any) => void;
112
+ onMessageClick?: (message: any, action: string) => void;
113
+ /** `action`: how it was dismissed (`close_button`, `backdrop`, `dismiss`, ...). */
114
+ onMessageDismiss?: (message: any, action: string) => void;
115
+ }
116
+
117
+ export interface InitConfig {
118
+ // Required
119
+ sdkKey: string;
120
+
121
+ // API Configuration
122
+ apiEndpoint?: string;
123
+
124
+ // User Identification
125
+ userId?: string;
126
+ anonymousId?: string;
127
+
128
+ // Batching & Performance
129
+ batchSize?: number;
130
+ batchFlushInterval?: number;
131
+ sendImmediately?: boolean;
132
+ maxQueueSize?: number;
133
+
134
+ // Session Management
135
+ sessionTimeout?: number; // Session timeout in milliseconds (default: 30 minutes)
136
+ trackSessionStart?: boolean; // Automatically track "Session Start" event on new sessions (default: true)
137
+ trackPageViews?: boolean; // Automatically track "Page Viewed" on page load (default: false)
138
+
139
+ // UTM Tracking
140
+ captureUTM?: boolean; // Automatically capture UTM parameters from URL (default: true)
141
+ resetSessionOnNewCampaign?: boolean; // Start new session when campaign changes (default: false)
142
+ trackPageProperties?: boolean; // Include referrer and landing page in events (default: true)
143
+ trackDeviceProperties?: boolean; // Include device info in events (default: true)
144
+
145
+ // In-App Messaging
146
+ inApp?: InAppOptions;
147
+
148
+ // Storage
149
+ persistQueue?: boolean;
150
+
151
+ // Network & Retry
152
+ maxRetries?: number;
153
+ retryBackoffMs?: number;
154
+ requestTimeout?: number;
155
+
156
+ // Privacy & GDPR
157
+ respectDoNotTrack?: boolean;
158
+ /**
159
+ * `true` opts this visitor out (persisted, exactly like `optOut()`).
160
+ * `false` or absent leaves a stored opt-out in place - only `optIn()` lifts it.
161
+ */
162
+ optOut?: boolean;
163
+ /**
164
+ * The consent banner's answer. Leave it UNSET when your site has no banner:
165
+ * with nothing stored the SDK then behaves as `granted`, and a `denied`
166
+ * stored by an earlier `updateConsent('denied')` stays in force.
167
+ *
168
+ * - `granted`: an explicit grant - lifts a stored `denied`.
169
+ * - `denied`: stored, and in force on later page views until a grant.
170
+ * - `pending`: this page view only, and never overrides a stored `denied`.
171
+ */
172
+ trackingConsent?: 'granted' | 'denied' | 'pending';
173
+
174
+ // SDK Authentication (optional, JWT-based).
175
+ // When enabled the host app supplies a customer-minted JWT which the SDK
176
+ // attaches as `X-Joryio-Auth` to every ingest request and refreshes on a
177
+ // 401 `sdk_authentication_error`. Opt-in — when disabled behaviour is unchanged.
178
+ enableSdkAuthentication?: boolean;
179
+ sdkAuthenticationToken?: string;
180
+
181
+ // Debugging
182
+ /** Log everything (`logLevel: 'debug'`) - unless `logLevel` is set too, which then wins. */
183
+ enableDebug?: boolean;
184
+ /**
185
+ * Console logging threshold. Nothing is logged unless this or `enableDebug`
186
+ * is set.
187
+ */
188
+ logLevel?: 'error' | 'warn' | 'info' | 'debug';
189
+
190
+ // Plugins (web only)
191
+ plugins?: Array<Plugin>;
192
+ }
193
+
194
+ export interface ResolvedConfig extends Required<Omit<InitConfig, 'userId' | 'plugins' | 'inApp' | 'sdkAuthenticationToken' | 'trackingConsent'>> {
195
+ userId?: string;
196
+ /** Undefined = not set by the app (see InitConfig.trackingConsent). */
197
+ trackingConsent?: 'granted' | 'denied' | 'pending';
198
+ plugins: Array<Plugin>;
199
+ inApp: Required<InAppOptions>;
200
+ // Optional — absent until the host app supplies a token.
201
+ sdkAuthenticationToken?: string;
202
+ /** Whether the logger prints at all: `enableDebug` or an explicit `logLevel`. */
203
+ loggingEnabled: boolean;
204
+ }
205
+
206
+ export interface Plugin {
207
+ name: string;
208
+ version: string;
209
+ load(sdk: any): void | Promise<void>;
210
+ unload(): void | Promise<void>;
211
+ beforeTrack?(event: any): any | null;
212
+ afterTrack?(event: any): void;
213
+ }
214
+
215
+ export interface TrackOptions {
216
+ timestamp?: Date | number;
217
+ sessionId?: string;
218
+ sendImmediately?: boolean;
219
+ }
220
+
221
+ export interface PageProperties {
222
+ path?: string;
223
+ title?: string;
224
+ url?: string;
225
+ referrer?: string;
226
+ search?: string;
227
+ [key: string]: any;
228
+ }
229
+
230
+ export interface EventProperties {
231
+ [key: string]: string | number | boolean | Date | null | undefined;
232
+ }
233
+
234
+ export interface UserTraits {
235
+ email?: string;
236
+ firstName?: string;
237
+ lastName?: string;
238
+ name?: string;
239
+ phone?: string;
240
+ [key: string]: any;
241
+ }
242
+
243
+ /**
244
+ * Keys that belong inside a nested group, listed so a misplaced one can be
245
+ * NAMED rather than ignored.
246
+ *
247
+ * A config key in the wrong place is the worst kind of SDK bug: everything
248
+ * initialises, no error is thrown, and the feature simply never happens. This
249
+ * cost a real integration - `allowHtmlJsInAppMessages: true` was set at the top
250
+ * level on a live site, silently dropped, and every HTML in-app campaign there
251
+ * displayed to nobody. It was only caught because the SDK started reporting its
252
+ * capabilities and the report said `content.native` alone.
253
+ *
254
+ * Warn, never guess. Silently promoting the key into `inApp` would be worse: it
255
+ * would make the working config and the broken one indistinguishable, so the
256
+ * snippet stays wrong everywhere it was copied to.
257
+ */
258
+ const NESTED_CONFIG_KEYS: Record<string, string> = {
259
+ allowHtmlJsInAppMessages: 'inApp',
260
+ evaluationMode: 'inApp',
261
+ syncInterval: 'inApp',
262
+ displayOptions: 'inApp',
263
+ };
264
+
265
+ function warnAboutMisplacedKeys(config: InitConfig): void {
266
+ const raw = config as unknown as Record<string, unknown>;
267
+ for (const [key, group] of Object.entries(NESTED_CONFIG_KEYS)) {
268
+ if (raw[key] === undefined) continue;
269
+ // eslint-disable-next-line no-console
270
+ console.warn(
271
+ `[Joryio] \`${key}\` must be nested under \`${group}\`, not passed at the ` +
272
+ `top level - as written it is IGNORED. Use: ` +
273
+ `{ ${group}: { ${key}: ${JSON.stringify(raw[key])} } }`,
274
+ );
275
+ }
276
+ // Removed in 1.3.0 because it never did anything; said out loud rather than
277
+ // left to look like it works.
278
+ if (raw.disablePlugins !== undefined) {
279
+ // eslint-disable-next-line no-console
280
+ console.warn(
281
+ '[Joryio] `disablePlugins` was removed in 1.3.0 and is ignored - leave a plugin out of `plugins` instead.',
282
+ );
283
+ }
284
+ }
285
+
286
+ export function resolveConfig(config: InitConfig): ResolvedConfig {
287
+ warnAboutMisplacedKeys(config);
288
+
289
+ // Resolve in-app options with defaults
290
+ const inAppOptions: Required<InAppOptions> = {
291
+ enabled: config.inApp?.enabled !== false,
292
+ // Defaults to differential-sync, not the removed `client-side`.
293
+ //
294
+ // The old default meant any integration that did not set this explicitly
295
+ // got the one mode that could not personalise - and, after the SDK Liquid
296
+ // engines were dropped, rendered nothing at all.
297
+ evaluationMode:
298
+ config.inApp?.evaluationMode === 'session-start-only' ? 'session-start-only' : 'differential-sync',
299
+ // 0 = no periodic re-sync. A positive value is clamped to the minimum so a
300
+ // typo (`syncInterval: 60` meaning seconds) cannot poll the API every 60 ms.
301
+ syncInterval:
302
+ config.inApp?.syncInterval && config.inApp.syncInterval > 0
303
+ ? Math.max(MIN_INAPP_SYNC_INTERVAL, config.inApp.syncInterval)
304
+ : 0,
305
+ displayOptions: {
306
+ defaultPosition: config.inApp?.displayOptions?.defaultPosition || 'center',
307
+ defaultAnimation: config.inApp?.displayOptions?.defaultAnimation || 'fade',
308
+ backdropColor: config.inApp?.displayOptions?.backdropColor || 'rgba(0, 0, 0, 0.5)',
309
+ // Unset = the close button takes the message's own text colour.
310
+ closeButtonColor: config.inApp?.displayOptions?.closeButtonColor || undefined,
311
+ zIndex: config.inApp?.displayOptions?.zIndex || 999999,
312
+ },
313
+ allowHtmlJsInAppMessages: config.inApp?.allowHtmlJsInAppMessages === true,
314
+ onMessageDisplay: config.inApp?.onMessageDisplay || (() => {}),
315
+ onMessageClick: config.inApp?.onMessageClick || (() => {}),
316
+ onMessageDismiss: config.inApp?.onMessageDismiss || (() => {}),
317
+ };
318
+
319
+ return {
320
+ sdkKey: config.sdkKey,
321
+ apiEndpoint: normalizeApiEndpoint(config.apiEndpoint),
322
+ userId: config.userId,
323
+ anonymousId: config.anonymousId || '',
324
+ batchSize: config.batchSize || DEFAULT_BATCH_SIZE,
325
+ batchFlushInterval:
326
+ typeof config.batchFlushInterval === 'number' && config.batchFlushInterval > 0
327
+ ? Math.max(MIN_FLUSH_INTERVAL, config.batchFlushInterval)
328
+ : DEFAULT_FLUSH_INTERVAL,
329
+ sendImmediately: config.sendImmediately || false,
330
+ maxQueueSize: config.maxQueueSize || DEFAULT_MAX_QUEUE_SIZE,
331
+ sessionTimeout: config.sessionTimeout || DEFAULT_SESSION_TIMEOUT,
332
+ trackSessionStart: config.trackSessionStart !== false,
333
+ trackPageViews: config.trackPageViews === true,
334
+ captureUTM: config.captureUTM !== false,
335
+ resetSessionOnNewCampaign: config.resetSessionOnNewCampaign || false,
336
+ trackPageProperties: config.trackPageProperties !== false,
337
+ trackDeviceProperties: config.trackDeviceProperties !== false,
338
+ inApp: inAppOptions,
339
+ persistQueue: config.persistQueue !== false,
340
+ // 0 is a real value ("no immediate retries"), so not `||`.
341
+ maxRetries:
342
+ typeof config.maxRetries === 'number' && config.maxRetries >= 0 ? Math.floor(config.maxRetries) : DEFAULT_MAX_RETRIES,
343
+ retryBackoffMs: config.retryBackoffMs || DEFAULT_RETRY_BACKOFF_MS,
344
+ requestTimeout:
345
+ config.requestTimeout && config.requestTimeout > 0 ? config.requestTimeout : DEFAULT_REQUEST_TIMEOUT,
346
+ respectDoNotTrack: config.respectDoNotTrack !== false,
347
+ optOut: config.optOut === true,
348
+ // NOT defaulted to 'granted': "unset" must stay distinguishable from an
349
+ // explicit grant, because only an explicit grant lifts a stored denial.
350
+ trackingConsent:
351
+ config.trackingConsent === 'granted' || config.trackingConsent === 'denied' || config.trackingConsent === 'pending'
352
+ ? config.trackingConsent
353
+ : undefined,
354
+ enableSdkAuthentication: config.enableSdkAuthentication === true,
355
+ sdkAuthenticationToken: config.sdkAuthenticationToken,
356
+ enableDebug: config.enableDebug || false,
357
+ // `enableDebug: true` alone means debug; an explicit `logLevel` wins (rule 58).
358
+ logLevel: config.logLevel || (config.enableDebug ? 'debug' : 'error'),
359
+ loggingEnabled: config.enableDebug === true || !!config.logLevel,
360
+ plugins: config.plugins || [],
361
+ };
362
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Device info helpers
3
+ */
4
+
5
+ export interface DeviceProperties {
6
+ $user_agent?: string;
7
+ $timezone?: string;
8
+ $screen_width?: number;
9
+ $screen_height?: number;
10
+ $viewport_width?: number;
11
+ $viewport_height?: number;
12
+ $language?: string;
13
+ $languages?: string[];
14
+ $platform?: string;
15
+ $browser?: string;
16
+ }
17
+
18
+ const detectBrowserName = (userAgent: string): string => {
19
+ if (userAgent.includes('Edg/')) return 'Edge';
20
+ if (userAgent.includes('OPR/')) return 'Opera';
21
+ if (userAgent.includes('Chrome/')) return 'Chrome';
22
+ if (userAgent.includes('Safari/') && !userAgent.includes('Chrome/')) return 'Safari';
23
+ if (userAgent.includes('Firefox/')) return 'Firefox';
24
+ if (userAgent.includes('MSIE') || userAgent.includes('Trident/')) return 'IE';
25
+ return 'Unknown';
26
+ };
27
+
28
+ export const getDeviceProperties = (): DeviceProperties => {
29
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
30
+ return {};
31
+ }
32
+
33
+ const userAgent = navigator.userAgent || '';
34
+ const language = navigator.language || '';
35
+ const languages = Array.isArray(navigator.languages) ? navigator.languages : undefined;
36
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
37
+
38
+ return {
39
+ $user_agent: userAgent,
40
+ $timezone: timezone,
41
+ $screen_width: window.screen?.width,
42
+ $screen_height: window.screen?.height,
43
+ $viewport_width: window.innerWidth,
44
+ $viewport_height: window.innerHeight,
45
+ $language: language,
46
+ $languages: languages,
47
+ $platform: navigator.platform,
48
+ $browser: detectBrowserName(userAgent),
49
+ };
50
+ };