@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.
- package/CHANGELOG.md +5 -0
- package/LICENSE +54 -0
- package/README.md +497 -2
- package/dist/core/config.d.ts +101 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/device.d.ts +14 -0
- package/dist/core/device.d.ts.map +1 -0
- package/dist/core/ecommerce.d.ts +67 -0
- package/dist/core/ecommerce.d.ts.map +1 -0
- package/dist/core/identity.d.ts +36 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/inapp.d.ts +169 -0
- package/dist/core/inapp.d.ts.map +1 -0
- package/dist/core/network.d.ts +158 -0
- package/dist/core/network.d.ts.map +1 -0
- package/dist/core/outbox.d.ts +79 -0
- package/dist/core/outbox.d.ts.map +1 -0
- package/dist/core/pending-attributes.d.ts +43 -0
- package/dist/core/pending-attributes.d.ts.map +1 -0
- package/dist/core/push.d.ts +59 -0
- package/dist/core/push.d.ts.map +1 -0
- package/dist/core/queue.d.ts +107 -0
- package/dist/core/queue.d.ts.map +1 -0
- package/dist/core/sdk.d.ts +169 -0
- package/dist/core/sdk.d.ts.map +1 -0
- package/dist/core/session.d.ts +36 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/storage.d.ts +17 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/user.d.ts +28 -0
- package/dist/core/user.d.ts.map +1 -0
- package/dist/core/utm.d.ts +39 -0
- package/dist/core/utm.d.ts.map +1 -0
- package/dist/core/values.d.ts +3 -0
- package/dist/core/values.d.ts.map +1 -0
- package/dist/core/write-retry.d.ts +66 -0
- package/dist/core/write-retry.d.ts.map +1 -0
- package/dist/index.d.mts +711 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/joryio-sw.js +6 -0
- package/dist/joryio.cjs.js +7265 -0
- package/dist/joryio.min.js +7 -0
- package/dist/joryio.min.js.map +1 -0
- package/dist/joryio.mjs +7254 -0
- package/dist/joryio.umd.js +7271 -0
- package/dist/utils/bucket-hash.d.ts +2 -0
- package/dist/utils/bucket-hash.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/uuid.d.ts +3 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/package.json +90 -4
- package/src/core/config.ts +362 -0
- package/src/core/device.ts +50 -0
- package/src/core/ecommerce.ts +482 -0
- package/src/core/identity.ts +251 -0
- package/src/core/inapp.ts +2419 -0
- package/src/core/network.ts +1261 -0
- package/src/core/outbox.ts +707 -0
- package/src/core/pending-attributes.ts +350 -0
- package/src/core/push.ts +667 -0
- package/src/core/queue.ts +894 -0
- package/src/core/sdk.ts +3108 -0
- package/src/core/session.ts +311 -0
- package/src/core/storage.ts +157 -0
- package/src/core/user.ts +203 -0
- package/src/core/utm.ts +304 -0
- package/src/core/values.ts +85 -0
- package/src/core/write-retry.ts +136 -0
- package/src/index.ts +305 -0
- package/src/snippet/loader.js +134 -0
- package/src/utils/bucket-hash.ts +44 -0
- package/src/utils/logger.ts +58 -0
- package/src/utils/uuid.ts +29 -0
- package/src/worker/joryio-sw.js +218 -0
|
@@ -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 @@
|
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
};
|