@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
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Joryio SDK Commercial License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joryio. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software development kit, including its source code, compiled forms,
|
|
6
|
+
documentation and samples (the "SDK"), is proprietary to Joryio
|
|
7
|
+
and is licensed, not sold.
|
|
8
|
+
|
|
9
|
+
1. Grant. Subject to the terms below, Joryio grants you a limited,
|
|
10
|
+
non-exclusive, non-transferable, non-sublicensable, revocable license to
|
|
11
|
+
use the SDK solely to integrate it into your own applications and websites
|
|
12
|
+
in order to use the Joryio service. This license is available only to
|
|
13
|
+
customers who hold an active Joryio subscription or another active written
|
|
14
|
+
agreement with Joryio, and only for as long as that subscription or
|
|
15
|
+
agreement remains in effect.
|
|
16
|
+
|
|
17
|
+
2. Distribution. You may distribute the SDK only as embedded in, and as an
|
|
18
|
+
integral part of, your own applications and websites that use the Joryio
|
|
19
|
+
service. You may not distribute the SDK on its own, as a library, or as
|
|
20
|
+
part of any toolkit, framework or product whose purpose is to let others
|
|
21
|
+
build software.
|
|
22
|
+
|
|
23
|
+
3. Restrictions. Except as expressly permitted above, you may not: (a) copy,
|
|
24
|
+
modify, publish, redistribute, sell, rent, lease or otherwise make the SDK
|
|
25
|
+
available to any third party; (b) sublicense the SDK; (c) use the SDK, or
|
|
26
|
+
any part of it, with any service other than the Joryio service, or to build
|
|
27
|
+
a product that competes with the Joryio service; or (d) remove or alter any
|
|
28
|
+
copyright, license or proprietary notice in the SDK. All rights not
|
|
29
|
+
expressly granted are reserved by Joryio.
|
|
30
|
+
|
|
31
|
+
4. Termination. This license ends automatically when your Joryio subscription
|
|
32
|
+
or agreement ends, or if you breach these terms. On termination you must
|
|
33
|
+
stop using the SDK and stop distributing new copies of applications or
|
|
34
|
+
websites that include it.
|
|
35
|
+
|
|
36
|
+
5. No warranty. The SDK is provided "as is" and "as available", without
|
|
37
|
+
warranty of any kind, express or implied, including any warranty of
|
|
38
|
+
merchantability, fitness for a particular purpose, title or
|
|
39
|
+
non-infringement, except as expressly set out in your agreement with
|
|
40
|
+
Joryio.
|
|
41
|
+
|
|
42
|
+
6. Limitation of liability. Joryio's liability arising out of or relating to
|
|
43
|
+
the SDK is limited as set out in your subscription or agreement with
|
|
44
|
+
Joryio. Where no such limitation applies, and to the maximum extent
|
|
45
|
+
permitted by law, Joryio will not be liable for any indirect, incidental,
|
|
46
|
+
special, consequential or punitive damages, or for any loss of profits,
|
|
47
|
+
revenue, data or goodwill.
|
|
48
|
+
|
|
49
|
+
7. Governing terms. Your use of the SDK is also governed by the Joryio Terms
|
|
50
|
+
of Service (https://joryio.com/terms) and by your agreement with Joryio. If
|
|
51
|
+
this license conflicts with your written agreement with Joryio, your
|
|
52
|
+
written agreement prevails.
|
|
53
|
+
|
|
54
|
+
Questions about this license: support@joryio.com
|
package/README.md
CHANGED
|
@@ -1,3 +1,498 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Joryio Web SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Joryio SDK for websites: identify visitors, track events, show in-app messages and send web push, all through your Joryio workspace.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Event tracking** with batching (50 events or 5 seconds by default), retry with exponential backoff, and a queue persisted to `localStorage` (memory fallback)
|
|
8
|
+
- **Identity** - anonymous ID out of the box, `identify`, and labelled aliases (`addAlias`)
|
|
9
|
+
- **User attributes** - set, increment, unset, add to and remove from arrays
|
|
10
|
+
- **In-app messages** displayed for you, with impressions, clicks and dismissals tracked automatically
|
|
11
|
+
- **Web push** through a service worker you host on your own origin
|
|
12
|
+
- **E-commerce helpers** that send the standard event names Joryio reports on
|
|
13
|
+
- **Subscription preferences** per channel and subscription group
|
|
14
|
+
- **Privacy controls** - opt-out, consent state, and Do Not Track honoured by default
|
|
15
|
+
- **SDK authentication** (optional) - a customer-minted JWT on every ingest request
|
|
16
|
+
- **UTM capture** and session tracking
|
|
17
|
+
- **TypeScript** definitions included
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- A Joryio workspace and a **web SDK key** (`jry_sdk_web_...`) from **Settings → Apps** in the dashboard. A mobile app's key (`jry_sdk_ios_...`, `jry_sdk_android_...`) is refused: the SDK logs why and stays inactive
|
|
22
|
+
- A modern browser (current Chrome, Edge, Firefox and Safari). Web push additionally needs a browser with the Push API and a secure (HTTPS) origin
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### Option 1: Async snippet (recommended)
|
|
27
|
+
|
|
28
|
+
Add this to your `<head>`. Calls made before the SDK finishes loading are queued and replayed in order. Every public method can be called this way, and `init` itself may come later - for example from your consent banner's callback - with the calls before it kept until it arrives. The queue holds at most 1,000 calls; beyond that the oldest are dropped - never `init`, `optOut()`, `optIn()`, `updateConsent()` or `wipeData()`.
|
|
29
|
+
|
|
30
|
+
Privacy decisions are the exception to "replayed in order": a queued `optOut()`, `optIn()` or `updateConsent()`, and a `wipeData()` queued before `init`, are applied **inside** `init`, before it sends anything - so `joryio.updateConsent('denied')` written above `joryio.init(...)` means no Session Start, in-app sync or push registration ever goes out. A queued `track()`, attribute write, `addAlias()` or `trackInAppConversion()` made while the visitor was opted out or consent was denied - by the stored choice, the init config or a decision queued before it - is dropped, and a later queued `optIn()` or `updateConsent('granted')` does not bring it back; `identify()` and unsubscribes are kept. The same goes for one queued before a queued `optOut()` or `updateConsent('denied')`, or before a `wipeData()` queued ahead of `init`. A `ready()` callback queued while tracking was off, or before a queued `optOut()` or `updateConsent('denied')`, still runs, but the `track()` calls, attribute writes, aliases, conversions and subscribes it makes while it runs are dropped too - unless the callback itself lifts what turned tracking off, for the rest of it: its own `optIn()` lifts an opt-out, its own `updateConsent('granted')` lifts a denial, neither lifts the other, and nothing inside it lifts an `optOut()` or denial queued after it. A `wipeData()` queued ahead of `init` does not gate a `ready()` callback queued before it (the callback runs for the fresh visitor). Reads and unsubscribes work, and the SDK instance it was handed behaves normally afterwards.
|
|
31
|
+
|
|
32
|
+
If `init` fails (a missing or malformed SDK key - the reason is logged to the console), nothing queued is lost: `window.joryio` stays the queue, and calling `joryio.init({...})` again with the right key starts the SDK with every call made so far, privacy decisions first, then the rest in order - an unsubscribe included.
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<script>
|
|
36
|
+
!function(){var e=window.joryio=window.joryio||[];if(!e.initialize)if(e.invoked)window.console&&console.error&&console.error("Joryio snippet included twice.");else{e.invoked=!0,e.methods=["init","track","identify","addAlias","setAttributes","setAttribute","incrementAttribute","unsetAttribute","addToArray","removeFromArray","reset","flush","optOut","optIn","updateConsent","getTrackingConsent","isUserOptedOut","setSdkAuthenticationToken","setSdkAuthenticationErrorCallback","getAnonymousId","getUserId","ready","getIdentity","getSessionInfo","getUTMData","updateUTM","syncInAppCampaigns","getInAppCampaigns","displayInAppCampaign","dismissInAppMessage","trackInAppConversion","ecommerce","wipeData","destroy"],e.keep=["init","optOut","optIn","updateConsent","wipeData"],e.factory=function(t){return function(){var n=Array.prototype.slice.call(arguments);if(n.unshift(t),e.length>=1e3)for(var i=0;i<e.length;i++)if(e.keep.indexOf(e[i][0])<0){e.splice(i,1);break}return e.push(n),/^(get|is)[A-Z]/.test(t)?void 0:e}};for(var t=0;t<e.methods.length;t++){var n=e.methods[t];e[n]=e.factory(n)}e.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://sdk.joryio.com/web/1/joryio.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n)},e.load()}}();
|
|
37
|
+
joryio.init({ sdkKey: 'jry_sdk_web_YOUR_SDK_KEY' });
|
|
38
|
+
</script>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A queued call cannot return a value before the SDK has loaded: a getter such as `getUserId()` or `isUserOptedOut()` returns `undefined` until then. To read an ID - or to use `user`, `push` or the tracker `ecommerce()` returns - use `ready`:
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
joryio.ready(function (sdk) {
|
|
45
|
+
var anonymousId = sdk.getAnonymousId();
|
|
46
|
+
var userId = sdk.getUserId(); // null until identify()
|
|
47
|
+
sdk.user.setSubscription('email', 'optedIn');
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Option 2: npm
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install @joryio/web-sdk
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
import JoryioSDK from '@joryio/web-sdk';
|
|
59
|
+
|
|
60
|
+
const joryio = new JoryioSDK({
|
|
61
|
+
sdkKey: 'jry_sdk_web_YOUR_SDK_KEY',
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Option 3: script tag
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<script src="https://sdk.joryio.com/web/1/joryio.min.js"></script>
|
|
69
|
+
<script>
|
|
70
|
+
// The script tag exposes the SDK class as window.Joryio
|
|
71
|
+
const joryio = new Joryio({
|
|
72
|
+
sdkKey: 'jry_sdk_web_YOUR_SDK_KEY',
|
|
73
|
+
});
|
|
74
|
+
</script>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### CDN paths
|
|
78
|
+
|
|
79
|
+
The SDK files are served from one global host, `https://sdk.joryio.com`:
|
|
80
|
+
|
|
81
|
+
| Path | What you get |
|
|
82
|
+
|------|--------------|
|
|
83
|
+
| `/web/1/<file>` | The newest 1.x release - recommended. Picks up fixes, never a breaking change |
|
|
84
|
+
| `/web/<version>/<file>` | One exact version, e.g. `/web/1.3.0/joryio.min.js`. Its contents never change |
|
|
85
|
+
|
|
86
|
+
Files: `joryio.min.js` (the SDK), `joryio-sw.js` (the web push service worker) and `joryio.umd.js` (unminified UMD build).
|
|
87
|
+
|
|
88
|
+
Only the file location is global. Events and API calls go to your region's API host (the `apiEndpoint` option).
|
|
89
|
+
|
|
90
|
+
## Quick start
|
|
91
|
+
|
|
92
|
+
### Initialize
|
|
93
|
+
|
|
94
|
+
With npm (`import JoryioSDK from '@joryio/web-sdk'`; `import { JoryioSDK }` works too). The package has an `exports` map with an ES module build for `import` and a CommonJS build for `require` (bundlers - webpack, esbuild, Rollup, Vite - use the ES module build for both, so an app that imports it in one file and requires it in another still gets one SDK), so the default import is the class in bundlers and in Node ESM (SSR, Vitest, `.mjs` scripts) alike, and TypeScript types it the same way with `moduleResolution` `node16`, `nodenext` or `bundler`. With the script tag the class is `window.Joryio` - write `new Joryio({...})` - and with the async snippet call `joryio.init({...})`.
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
const joryio = new JoryioSDK({
|
|
98
|
+
sdkKey: 'jry_sdk_web_YOUR_SDK_KEY',
|
|
99
|
+
apiEndpoint: 'https://api-eu1.joryio.com/api', // your region's API host; a bare host also works, /api is added
|
|
100
|
+
enableDebug: false,
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Track events
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
joryio.track('Button Clicked');
|
|
108
|
+
|
|
109
|
+
joryio.track('Product Viewed', {
|
|
110
|
+
productId: 'abc123',
|
|
111
|
+
category: 'Electronics',
|
|
112
|
+
price: 299.99,
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Identify users
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
joryio.identify('user-123');
|
|
120
|
+
joryio.setAttributes({
|
|
121
|
+
email: 'user@example.com',
|
|
122
|
+
name: 'Jane Doe',
|
|
123
|
+
plan: 'premium',
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### User attributes
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
joryio.setAttributes({ totalPurchases: 5 });
|
|
131
|
+
joryio.setAttribute('language', 'en');
|
|
132
|
+
joryio.incrementAttribute('pageViews', 1);
|
|
133
|
+
joryio.unsetAttribute('temporaryFlag');
|
|
134
|
+
joryio.addToArray('interests', 'running');
|
|
135
|
+
joryio.removeFromArray('interests', 'cycling');
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Reset on logout
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
// Clears the user identity and starts a fresh anonymous session
|
|
142
|
+
joryio.reset();
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The new anonymous ID is in place as soon as `reset()` returns - the returned promise only covers delivering what was queued - so `reset(); identify(nextUser)` is safe without `await`. The previous user's SDK authentication token is forgotten. Their attribute, alias and subscription writes that were not acknowledged yet are still sent, with their own IDs, never the next visitor's. Calling `identify('B')` while user A is signed in, without `reset()`, does the same for A's queued writes and token.
|
|
146
|
+
|
|
147
|
+
A `null` or `undefined` value in `setAttributes` / `setAttribute` removes the attribute, the same as `unsetAttribute`.
|
|
148
|
+
|
|
149
|
+
A value JSON cannot carry - `NaN`, `Infinity`, a `BigInt`, a function, an object that refers to itself - is dropped where it enters, with a warning in the log: that attribute key (or that event property) is left out and the rest of the call goes ahead. One bad value never holds up other writes.
|
|
150
|
+
|
|
151
|
+
## Configuration options
|
|
152
|
+
|
|
153
|
+
| Option | Type | Default | Description |
|
|
154
|
+
|--------|------|---------|-------------|
|
|
155
|
+
| `sdkKey` | `string` | **required** | Your web SDK key |
|
|
156
|
+
| `apiEndpoint` | `string` | `https://api-eu1.joryio.com/api` | Your region's API host. A bare host works; `/api` is added |
|
|
157
|
+
| `userId` | `string` | - | Start with a known user ID |
|
|
158
|
+
| `anonymousId` | `string` | generated | Use your own anonymous ID |
|
|
159
|
+
| `batchSize` | `number` | `50` | Events per request (at most 500, the API's limit) |
|
|
160
|
+
| `batchFlushInterval` | `number` | `5000` | Flush interval in ms (at least `1000`; lower values are raised to it) |
|
|
161
|
+
| `sendImmediately` | `boolean` | `false` | Send each event immediately instead of batching |
|
|
162
|
+
| `maxQueueSize` | `number` | `1000` | Most events held while sends fail. The oldest are dropped beyond it - the only way an undelivered event is ever dropped |
|
|
163
|
+
| `persistQueue` | `boolean` | `true` | Persist the queue to `localStorage` |
|
|
164
|
+
| `maxRetries` | `number` | `3` | Immediate retries of one request (5xx, 429, network error). A batch that still fails is kept and retried later |
|
|
165
|
+
| `retryBackoffMs` | `number` | `1000` | Initial retry backoff in ms; doubles per failure, up to 5 minutes |
|
|
166
|
+
| `requestTimeout` | `number` | `10000` | Timeout in ms for each API request (events, attributes, in-app, push) |
|
|
167
|
+
| `sessionTimeout` | `number` | `1800000` | Inactivity (ms) before a new session starts. Checked on every `track()` and when the tab becomes visible again |
|
|
168
|
+
| `trackSessionStart` | `boolean` | `true` | Track one "Session Start" per new session (a reload that resumes the session sends none) |
|
|
169
|
+
| `trackPageViews` | `boolean` | `false` | Track "Page Viewed" on page load |
|
|
170
|
+
| `trackPageProperties` | `boolean` | `true` | Include referrer and landing page in events |
|
|
171
|
+
| `trackDeviceProperties` | `boolean` | `true` | Include device info in events |
|
|
172
|
+
| `captureUTM` | `boolean` | `true` | Capture UTM parameters from the URL |
|
|
173
|
+
| `resetSessionOnNewCampaign` | `boolean` | `false` | Start a new session when the UTM campaign changes |
|
|
174
|
+
| `respectDoNotTrack` | `boolean` | `true` | Honour the browser's Do Not Track setting |
|
|
175
|
+
| `optOut` | `boolean` | `false` | `true` opts the visitor out (remembered, like `optOut()`). `false` does not lift a stored opt-out - only `optIn()` does |
|
|
176
|
+
| `trackingConsent` | `string` | unset | `'granted'`, `'denied'` or `'pending'`. Leave it unset if your site has no consent banner - see [Privacy controls](#privacy-controls) |
|
|
177
|
+
| `enableSdkAuthentication` | `boolean` | `false` | Attach and refresh a customer-minted `X-Joryio-Auth` JWT (see [SDK authentication](#sdk-authentication-optional)) |
|
|
178
|
+
| `sdkAuthenticationToken` | `string` | - | Initial JWT, used when `enableSdkAuthentication` is true |
|
|
179
|
+
| `inApp` | `object` | see below | In-app messaging options |
|
|
180
|
+
| `enableDebug` | `boolean` | `false` | Log everything to the console (`logLevel: 'debug'`). An explicit `logLevel` wins |
|
|
181
|
+
| `logLevel` | `string` | - | `'error'`, `'warn'`, `'info'` or `'debug'`. Nothing is logged unless this or `enableDebug` is set |
|
|
182
|
+
| `plugins` | `Plugin[]` | `[]` | SDK plugins (web only - see [Plugins](#plugins)) |
|
|
183
|
+
|
|
184
|
+
`inApp` options:
|
|
185
|
+
|
|
186
|
+
| Option | Type | Default | Description |
|
|
187
|
+
|--------|------|---------|-------------|
|
|
188
|
+
| `enabled` | `boolean` | `true` | Turn in-app messaging on or off |
|
|
189
|
+
| `evaluationMode` | `string` | `'differential-sync'` | When campaigns are fetched. `'differential-sync'`: on load, `identify()`, `reset()`, a new session, a change to a watched attribute, and every `syncInterval`. `'session-start-only'`: on load, `identify()`, `reset()` and a new session only |
|
|
190
|
+
| `syncInterval` | `number` | `0` (off) | Re-fetch campaigns every N ms in `'differential-sync'` mode. Values below `30000` are raised to `30000`; paused while the tab is hidden |
|
|
191
|
+
| `allowHtmlJsInAppMessages` | `boolean` | `false` | Allow HTML in-app messages (see below) |
|
|
192
|
+
| `displayOptions.defaultPosition` | `string` | `'center'` | `'top'`, `'center'` or `'bottom'`: where a `custom`-type message sits. Modal, banner, slide-up and full-screen messages keep their own placement |
|
|
193
|
+
| `displayOptions.defaultAnimation` | `string` | `'fade'` | `'fade'`, `'slide'` or `'none'`: how messages enter. Skipped when the visitor's system asks for reduced motion |
|
|
194
|
+
| `displayOptions.backdropColor` | `string` | `'rgba(0, 0, 0, 0.5)'` | Colour of the dim layer behind modal and full-screen messages |
|
|
195
|
+
| `displayOptions.closeButtonColor` | `string` | the message's text colour | Colour of the close button |
|
|
196
|
+
| `displayOptions.zIndex` | `number` | `999999` | Stacking order of messages (the backdrop sits one below) |
|
|
197
|
+
| `onMessageDisplay` / `onMessageClick` / `onMessageDismiss` | `function` | - | Callbacks for each lifecycle step. A callback that throws is logged and ignored; the SDK's own bookkeeping carries on |
|
|
198
|
+
|
|
199
|
+
## In-app messaging
|
|
200
|
+
|
|
201
|
+
The SDK displays in-app messages for you. Eligible campaigns appear on their own, and impressions, clicks and dismissals are tracked automatically.
|
|
202
|
+
|
|
203
|
+
Messages arrive in one of two shapes:
|
|
204
|
+
|
|
205
|
+
- **Native** - structured data (headline, body, image, buttons) written into plain DOM elements as text nodes. No iframe, no script execution.
|
|
206
|
+
- **HTML** - author-supplied markup and CSS, shown in a sandboxed iframe.
|
|
207
|
+
|
|
208
|
+
**HTML messages are off by default.** Joryio strips scripts and event handlers from HTML messages before sending them, but the iframe runs the SDK's own bootstrap and can execute script at your page's origin, so the residual risk is a sanitiser bypass. Opting in is a decision for your own team:
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
const joryio = new JoryioSDK({
|
|
212
|
+
sdkKey: 'jry_sdk_web_YOUR_SDK_KEY',
|
|
213
|
+
inApp: {
|
|
214
|
+
allowHtmlJsInAppMessages: true, // default: false
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Leaving it off does **not** disable in-app messaging: native messages still display. HTML campaigns are skipped (with a warning in the log when logging is on - see `logLevel`).
|
|
220
|
+
|
|
221
|
+
An HTML message can never navigate its own frame: links and redirects go through the SDK, which opens them on your page (`http(s)` and root-relative links only). The frame may load only the message itself - if it navigates anyway, the message is closed - and the message's CSS cannot end its `<style>` element.
|
|
222
|
+
|
|
223
|
+
If your Content Security Policy forbids inline scripts or framed content, leave this off and author your campaigns as native messages.
|
|
224
|
+
|
|
225
|
+
Manual control, when you need it:
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
await joryio.syncInAppCampaigns(); // fetch campaigns now
|
|
229
|
+
const campaigns = joryio.getInAppCampaigns(); // what is currently eligible
|
|
230
|
+
await joryio.displayInAppCampaign(campaignId); // show one now
|
|
231
|
+
joryio.dismissInAppMessage(campaignId);
|
|
232
|
+
await joryio.trackInAppConversion(campaignId, 'Order Completed');
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Web push
|
|
236
|
+
|
|
237
|
+
Web push needs a service worker on **your own origin** (browsers require it). Host a file named `joryio-sw.js` at the root of your site containing one line:
|
|
238
|
+
|
|
239
|
+
```javascript
|
|
240
|
+
importScripts('https://sdk.joryio.com/web/1/joryio-sw.js');
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Then ask for permission from a user gesture, such as a button click:
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
if (joryio.push.isSupported() && joryio.push.getPermissionState() === 'notDetermined') {
|
|
247
|
+
button.onclick = () => joryio.push.subscribe();
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
| Method | Description |
|
|
252
|
+
|--------|-------------|
|
|
253
|
+
| `push.isSupported()` | Whether this browser can do web push at all |
|
|
254
|
+
| `push.getPermissionState()` | `'granted'`, `'denied'`, `'notDetermined'` or `'unavailable'` - the same names as every Joryio SDK |
|
|
255
|
+
| `push.prepare(options?)` | Pre-register the service worker so the later `subscribe()` is fast |
|
|
256
|
+
| `push.subscribe(options?)` | Prompt (if needed), subscribe and register this browser with Joryio |
|
|
257
|
+
| `push.getExistingSubscription(options?)` | The current subscription, without prompting |
|
|
258
|
+
| `push.unsubscribe(options?)` | Remove this browser's subscription and unregister it with Joryio. It stays unsubscribed on later visits until `subscribe()` is called again. Never rejects; resolves `false` when nothing could be removed |
|
|
259
|
+
|
|
260
|
+
`options` takes `swPath` (default `'/joryio-sw.js'`) and `scope` (default `'/'`) if you host the worker elsewhere.
|
|
261
|
+
|
|
262
|
+
You do not need to call `subscribe()` again on later visits. When permission is already granted, the SDK checks the browser's subscription on load and registers it again if the browser rotated it, if your workspace's VAPID key changed (it re-subscribes without a prompt), or after `reset()` (so the subscription follows the new anonymous visitor rather than the user who logged out) - unless `unsubscribe()` was called. Each registration carries the SDK's stable device ID. Nothing is registered while tracking is opted out or consent is pending.
|
|
263
|
+
|
|
264
|
+
The service worker shows the push with its title, body, icon, image, badge, actions, `tag` (a newer push with the same tag replaces the one on screen) and `requireInteraction` (stays until dismissed), as the campaign set them.
|
|
265
|
+
|
|
266
|
+
The service worker shows each push, reports its delivery and records the click. It reads these keys from the push's `data`, the same names the Android and iOS SDKs read:
|
|
267
|
+
|
|
268
|
+
| Key | Effect |
|
|
269
|
+
|-----|--------|
|
|
270
|
+
| `trackingId` | Used for the delivery receipt and for click tracking. Absent on test sends, which are not counted |
|
|
271
|
+
| `jry_deep_link` | The page a click opens (the site root when absent) |
|
|
272
|
+
| `jry_image_url` | Large image, in browsers that show one |
|
|
273
|
+
| `jry_api_origin` | Where the worker sends the receipt and the click |
|
|
274
|
+
| `jry_campaign_id` | The campaign id, or the journey id for a journey send. Informational |
|
|
275
|
+
|
|
276
|
+
## E-commerce
|
|
277
|
+
|
|
278
|
+
The e-commerce helper sends the standard event names that Joryio's revenue reporting and attribution read, so you do not have to spell them yourself:
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
const ecommerce = joryio.ecommerce({ currency: 'USD' });
|
|
282
|
+
|
|
283
|
+
ecommerce.viewProduct({ productId: 'SKU123', name: 'Blue Shirt', price: 29.99 });
|
|
284
|
+
ecommerce.addToCart({ productId: 'SKU123', name: 'Blue Shirt', price: 29.99, quantity: 1 });
|
|
285
|
+
ecommerce.purchase({
|
|
286
|
+
orderId: 'ORD123',
|
|
287
|
+
items: [{ productId: 'SKU123', name: 'Blue Shirt', price: 29.99, quantity: 1 }],
|
|
288
|
+
value: 29.99,
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Also available: `viewCategory`, `search`, `removeFromCart`, `cartViewed`, `updateCart`, `startCheckout`, `addPaymentInfo`, `orderFulfilled`, `orderDelivered`, `orderCancelled`, `orderRefunded`, `addToWishlist`, `removeFromWishlist`, `shareProduct`, `applyCoupon`, `removeCoupon` and `submitReview`. See [E-commerce tracking](https://docs.joryio.com/developers/sdk/ecommerce-tracking).
|
|
293
|
+
|
|
294
|
+
Every helper checks its required arguments - a `productId`, an `orderId`, a finite order `value`, an `items` array. When one is missing it logs a warning and sends nothing (never an empty or zero-value order), and it never throws into your page. `ecommerce()` takes one option, `currency` (default `'USD'`).
|
|
295
|
+
|
|
296
|
+
## Advanced usage
|
|
297
|
+
|
|
298
|
+
### Plugins
|
|
299
|
+
|
|
300
|
+
Plugins are a **web-only** feature: the mobile, React Native and Unity SDKs have no plugin API. A plugin can inspect, change or block every event before it is queued.
|
|
301
|
+
|
|
302
|
+
```javascript
|
|
303
|
+
const myPlugin = {
|
|
304
|
+
name: 'my-plugin',
|
|
305
|
+
version: '1.0.0',
|
|
306
|
+
load(sdk) {},
|
|
307
|
+
unload() {},
|
|
308
|
+
beforeTrack(event) {
|
|
309
|
+
// Modify the event, or return null to block it
|
|
310
|
+
return event;
|
|
311
|
+
},
|
|
312
|
+
afterTrack(event) {},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const joryio = new JoryioSDK({
|
|
316
|
+
sdkKey: 'jry_sdk_web_YOUR_SDK_KEY',
|
|
317
|
+
plugins: [myPlugin],
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
To turn a plugin off, leave it out of `plugins`. A plugin hook that throws is logged and the event is kept unchanged.
|
|
322
|
+
|
|
323
|
+
### Flushing
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
await joryio.flush();
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`flush()` sends now, skipping the SDK's own retry backoff - but never a `Retry-After` the API asked for.
|
|
330
|
+
|
|
331
|
+
### Privacy controls
|
|
332
|
+
|
|
333
|
+
Two independent switches decide whether anything is collected: **consent** (your cookie banner's answer) and the visitor's own **opt-out**. The SDK collects only when consent is granted **and** the visitor has not opted out - any "no" wins.
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
joryio.updateConsent('granted'); // or 'denied' or 'pending'
|
|
337
|
+
joryio.optOut(); // remembered across page loads until optIn()
|
|
338
|
+
joryio.optIn();
|
|
339
|
+
|
|
340
|
+
joryio.ready(function (sdk) {
|
|
341
|
+
sdk.getTrackingConsent(); // 'granted' | 'denied' | 'pending'
|
|
342
|
+
sdk.isUserOptedOut(); // the opt-out switch only
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
| State | What the SDK does |
|
|
347
|
+
|-------|-------------------|
|
|
348
|
+
| Consent `granted` (or never set, with no stored denial) | Collects and sends normally |
|
|
349
|
+
| Consent `pending` | Collects locally and sends **nothing** - no Session Start, no in-app sync, no push registration - until `updateConsent('granted')` releases it all. `updateConsent('denied')` discards it. What is held, all kept in `localStorage` so a reload does not lose it: events and `identify` (up to `maxQueueSize` events), attribute writes (up to 500) and alias and subscription changes (up to 200). Unsubscribes are the exception: they are sent anyway. The web push service worker does not report deliveries or clicks meanwhile |
|
|
350
|
+
| Consent `denied` | Collects nothing and clears anything queued. The service worker stops reporting deliveries and clicks |
|
|
351
|
+
| `optOut()` / `optOut: true` | Collects nothing, clears queued events and pending attribute, alias and subscribe writes (unsubscribes are still delivered), and is stored so it survives a reload. The web push service worker still shows notifications but stops reporting deliveries and clicks |
|
|
352
|
+
| Do Not Track | Treated as opted out while `respectDoNotTrack` is true |
|
|
353
|
+
|
|
354
|
+
How the two switches behave:
|
|
355
|
+
|
|
356
|
+
- **`denied` is remembered.** It stays in force on later page views until `updateConsent('granted')` or an explicit `trackingConsent: 'granted'` in the init config. Leaving `trackingConsent` unset keeps a stored denial - which is why it has no default.
|
|
357
|
+
- **`pending` is for one page view.** It is never stored, and never overrides a stored denial.
|
|
358
|
+
- **Consent never lifts an opt-out.** `updateConsent('granted')` does not undo `optOut()`; only `optIn()` does. `optOut()` and `optIn()` never change the stored consent.
|
|
359
|
+
- **The profile records the opt-out.** When the opt-out switch changes - including `optOut: true` at init - the SDK writes `tracking_opted_out` (`true` or `false`) to the profile, with the IDs of the visitor who made the choice, and sends nothing while it is unchanged. It is retried by the rule under [Delivery](#delivery). `true` is a withdrawal and is sent even while consent is denied or pending, or after `wipeData()`; `false` waits until tracking is allowed.
|
|
360
|
+
- **A session that began while tracking was off** gets its one Session Start when consent is granted or the visitor opts back in.
|
|
361
|
+
- **`identify()` while tracking is off** (consent denied, or opted out) sends nothing and writes nothing to a profile. The user ID is kept on this device only, so a later unsubscribe or opt-out reaches that person rather than an anonymous profile; the normal `identify()` runs when tracking is allowed again - before the Session Start of a session that began meanwhile, so that session is the new user's. It is used for every withdrawal: an unsubscribe, a group unsubscribe, the opt-out marker and `push.unsubscribe()`. An `identify()` once tracking is allowed replaces it, an explicit `userId` in the init config wins over it, and `reset()` and `wipeData()` forget it.
|
|
362
|
+
- **Every open tab follows.** An opt-out, a consent change, a `reset()` or an `identify()` made in one tab of your site applies to its other open tabs at once, not at their next reload. A logout in one tab is a full `reset()` in the others: they move to the new visitor and the new session, and writes they made for the outgoing user stay that user's. A grant in one tab also releases a tab waiting in `pending`. Another tab or script calling `localStorage.clear()` never lifts an opt-out or a denial: the tab keeps it and stores it again. The profile writes and logged-out users' tokens every tab keeps in `localStorage` are merged, never overwritten by one tab. The user an `identify()` keeps for withdrawals while tracking is off is followed too. An explicit `updateConsent('granted')` or `optIn()` made right after such a `localStorage.clear()` is newer than the stored decision and wins in every tab. A profile write that another tab has already delivered, or replaced with a newer one, is never sent by this tab, and `wipeData()` in one tab discards the other tabs' waiting writes too (withdrawals excepted).
|
|
363
|
+
|
|
364
|
+
With the async snippet, `optOut()`, `optIn()`, `updateConsent()` and a `wipeData()` called before `init` take effect **before** the SDK starts - nothing is sent under the state they replace. See [Option 1: Async snippet](#option-1-async-snippet-recommended).
|
|
365
|
+
|
|
366
|
+
### Erasing local data
|
|
367
|
+
|
|
368
|
+
```javascript
|
|
369
|
+
await joryio.wipeData();
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Erases everything the SDK keeps about the visitor in this browser: the identity (a new anonymous ID, no user ID), queued events and a pending `identify`, pending attribute, alias and subscription writes, in-app state and impression history, the session, UTM data, the device ID and the push subscription (unregistered with Joryio as well). Withdrawals still waiting - an unsubscribe, the opt-out marker, the push unregister - are still delivered, with the IDs they were made with. It **keeps** the opt-out and a denied consent, and does not opt anyone out - that is `optOut()`. Tracking continues as a new anonymous visitor, in a new session.
|
|
373
|
+
|
|
374
|
+
### Aliases
|
|
375
|
+
|
|
376
|
+
```javascript
|
|
377
|
+
// A labelled alias, e.g. a CRM ID. Works before identify() too:
|
|
378
|
+
// the alias is attached to the anonymous visitor's profile.
|
|
379
|
+
joryio.addAlias('crm', 'crm_98765');
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
To link an anonymous visitor to their user ID, call `identify()` - it merges the anonymous profile into the user's.
|
|
383
|
+
|
|
384
|
+
### SDK authentication (optional)
|
|
385
|
+
|
|
386
|
+
For hardened deployments you can require every ingest request to carry a JWT that your backend mints. When enabled, the SDK attaches it as an `X-Joryio-Auth` header to every ingest request (event batches, identify, alias, attributes, in-app sync and track, and push) and refreshes it when the backend rejects it. It is opt-in: when disabled, nothing changes.
|
|
387
|
+
|
|
388
|
+
Your backend signs the JWT with a key you register with Joryio, with a `sub` matching the identified user. A bad, expired or missing token is rejected with `401 { error: 'sdk_authentication_error', reason: 'missing' | 'invalid' | 'expired' | 'sub_mismatch' }`.
|
|
389
|
+
|
|
390
|
+
```javascript
|
|
391
|
+
const joryio = new JoryioSDK({
|
|
392
|
+
sdkKey: 'jry_sdk_web_YOUR_SDK_KEY',
|
|
393
|
+
enableSdkAuthentication: true,
|
|
394
|
+
sdkAuthenticationToken: await mintJoryioJwt(),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Fires on a 401 sdk_authentication_error. Return a fresh token (or call
|
|
398
|
+
// setSdkAuthenticationToken); the failed request is then retried once.
|
|
399
|
+
joryio.setSdkAuthenticationErrorCallback(async (info) => {
|
|
400
|
+
// info = { reason, endpoint, userId }
|
|
401
|
+
return await mintJoryioJwt();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Update the token at any time, e.g. before it expires
|
|
405
|
+
joryio.setSdkAuthenticationToken(await mintJoryioJwt());
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
- **The callback fires on an SDK-auth `401` for the current user's request** while `enableSdkAuthentication` is on - including before any token was set (`reason: 'missing'`). It never fires for a request of a user who has since logged out, so it cannot make your app sign out the person now using it.
|
|
409
|
+
- **One immediate retry, outside the normal backoff.** If no callback is registered, the callback returns no token, or the retry fails too, the request is not dropped: the events stay queued and are retried with the normal backoff, carrying whatever token is set by then. A profile write is retried at most 10 times.
|
|
410
|
+
- **A logged-out user's token is kept for their leftovers.** At `reset()` - or `identify()` of another user - the token stops being the session's but stays bound to the outgoing user's queued events and writes, kept in `localStorage` with them so a reload does not lose it, and deleted once the last of them is delivered or dropped - no sooner than a minute after it was set aside, and never later than 30 days. Setting a token whose `sub` is another user does the same to the token it replaces, so the order does not matter: a token you set for the incoming user before calling `identify()` is recognised by its `sub` and stays theirs, and the outgoing user's leftovers keep the outgoing user's token. Refreshing the token of the same user sets nothing aside. A token whose `sub` is not the current user's is kept for that user this way also when no one is signed in, until `identify()` makes it the session's; `wipeData()` deletes such a token.
|
|
411
|
+
- **One user per request.** Events are never batched across a change of user, so a logged-out user's queued events cannot hold up the next user's. A request carries only a token whose `sub` is that request's user: the current token when it is theirs, else the token set aside for them, else none - so a token you set for the next user before `identify()` never goes out on the current user's requests. Anonymous requests carry no token.
|
|
412
|
+
- **A logged-out user's leftovers are dropped, not retried forever.** `reset()` takes the token off the session; it stays with the outgoing user's own queued work. Events and writes the outgoing user had already queued still go out with that user's token, so a normal logout loses nothing. Events, an `identify` and profile writes of a user who is no longer the current one, refused with any SDK-auth `401` (`missing`, `sub_mismatch`, `expired` or `invalid`), are dropped: no token for that user will be set again.
|
|
413
|
+
- **In-app requests** carry the header too; a 401 there triggers a refresh so the next sync carries a valid token.
|
|
414
|
+
|
|
415
|
+
### Subscription preferences
|
|
416
|
+
|
|
417
|
+
```javascript
|
|
418
|
+
import { SubscriptionStatus } from '@joryio/web-sdk';
|
|
419
|
+
|
|
420
|
+
joryio.user.setSubscription('email', SubscriptionStatus.OPTED_IN);
|
|
421
|
+
joryio.user.setSubscriptions({
|
|
422
|
+
email: SubscriptionStatus.OPTED_IN,
|
|
423
|
+
sms: SubscriptionStatus.UNSUBSCRIBED,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
joryio.user.addToSubscriptionGroup('newsletter-list-id', 'email');
|
|
427
|
+
joryio.user.removeFromSubscriptionGroup('newsletter-list-id', 'email');
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Channels: `email`, `sms`, `whatsapp`, `push`, `viber`.
|
|
431
|
+
|
|
432
|
+
| Status | Meaning |
|
|
433
|
+
|--------|---------|
|
|
434
|
+
| `SubscriptionStatus.OPTED_IN` | Explicitly opted in (double opt-in confirmed) |
|
|
435
|
+
| `SubscriptionStatus.SUBSCRIBED` | Subscribed (single opt-in) |
|
|
436
|
+
| `SubscriptionStatus.UNSUBSCRIBED` | Opted out |
|
|
437
|
+
|
|
438
|
+
The `user` object is write-only: there are no getters for subscription state. Changes are kept and delivered in order (see [Delivery](#delivery)); a change made before `identify()` is sent with the user ID once the identify has reached the server, so SDK authentication accepts it. An unsubscribe is delivered even while the visitor is opted out or consent is denied.
|
|
439
|
+
|
|
440
|
+
### Delivery
|
|
441
|
+
|
|
442
|
+
- Events go out in requests of at most `batchSize` (and never more than 500 events or about 900 KB).
|
|
443
|
+
- A request the API refuses as malformed (400, 413, 422) is split in half and retried, until the one bad event is found and dropped (logged as an error when logging is on). It never holds up the events behind it.
|
|
444
|
+
- Events the API would refuse for good are dropped by `track()` before they are queued, with a log line: a name that is not 1-255 characters, or properties over the API's limits - more than 50 KB serialized, more than 200 keys, or nested deeper than 5 levels.
|
|
445
|
+
- If the API stores only some events of a batch, only the ones it reports as failed (`failedIndices`) are retried. Events it reports as refused for good (`rejectedIndices`) are dropped.
|
|
446
|
+
- Attribute writes keep call order per attribute: two `addToArray()` calls on one key both arrive, and a later write to a key never shares a request with an earlier one. A 2xx response whose body says `success: false` - for a batch, identify, attributes, an alias, a subscription change or a push registration - counts as a failure and is retried, never treated as done.
|
|
447
|
+
- Profile writes - attributes, `addAlias()`, subscription and subscription-group changes - are kept in `localStorage` until the API acknowledges them, so a navigation or a closed tab does not lose them; at page unload the ones waiting are handed to `fetch(..., { keepalive: true })` as well. A newer change to the same channel, group or attribute replaces an older one still waiting, so an older change never lands after a newer one.
|
|
448
|
+
- A write keeps the identity it was made under: one made just before `identify()` is sent with that user ID once the identify has reached the server; one of user A still waiting when `identify('B')` or `reset()` is called still goes to A.
|
|
449
|
+
- One retry rule for every write (`identify()` included): network errors, timeouts, 429, 5xx and 402 never count against it and are retried with a growing backoff; a 404 ("no profile yet") is retried at most 5 times and any other retryable refusal (`success: false`, an SDK-auth 401 for the current user) at most 10 times; 400, 403, 409, 413 and 422 drop the write with a log line.
|
|
450
|
+
- Network errors, timeouts, 429 and 5xx keep every event and retry with exponential backoff (honouring `Retry-After`), and immediately when the browser comes back online.
|
|
451
|
+
- A 402 (event ingestion paused for your workspace), 403 or an invalid key stops sending events for the rest of the page view. Queued events are kept for the next one. After a 402, profile writes wait too - except withdrawals, which the API still accepts and which are still sent.
|
|
452
|
+
- When the page is closed, queued events are handed to the browser with `fetch(..., { keepalive: true })` and your SDK auth header. Anything that does not fit the browser's 64 KB limit - one budget for the whole page, however many unload events fire - stays queued for the next visit. While the browser is offline, or after the last send failed, nothing is handed off: the events stay queued rather than ride a request that is likely to fail.
|
|
453
|
+
- Event timestamps are ISO 8601 in UTC with milliseconds (`2026-10-07T09:30:00.123Z`), as on every Joryio SDK.
|
|
454
|
+
- Subscription and alias changes wait for a pending `identify()` to reach the server first, carry both your user ID and the anonymous ID, and are retried if the contact does not exist yet.
|
|
455
|
+
- **Withdrawals are always delivered**: an unsubscribe (`SubscriptionStatus.UNSUBSCRIBED`, `removeFromSubscriptionGroup()`), the `tracking_opted_out` marker and a push unregister are sent even while the visitor is opted out, with consent denied or pending, and after `wipeData()`. Every other write stops with them.
|
|
456
|
+
|
|
457
|
+
### Teardown
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
await joryio.destroy(); // flush, stop every timer and remove listeners
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The SDK never throws into your page. A missing or malformed `sdkKey` is logged once (always, whatever the log level) and the instance ignores every call; a `null` argument where one is required is logged and ignored.
|
|
464
|
+
|
|
465
|
+
## TypeScript
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import JoryioSDK, { InitConfig, Plugin } from '@joryio/web-sdk';
|
|
469
|
+
|
|
470
|
+
const config: InitConfig = {
|
|
471
|
+
sdkKey: 'jry_sdk_web_YOUR_SDK_KEY',
|
|
472
|
+
enableDebug: true,
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const sdk = new JoryioSDK(config);
|
|
476
|
+
sdk.track('Event', { property: 'value' });
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## Bundle size
|
|
480
|
+
|
|
481
|
+
`joryio.min.js` 1.3.0 is about 167 KB minified, about 45 KB gzipped.
|
|
482
|
+
|
|
483
|
+
## Documentation
|
|
484
|
+
|
|
485
|
+
- [Web SDK guide](https://docs.joryio.com/developers/sdk/web-sdk)
|
|
486
|
+
- [UTM and attribution](https://docs.joryio.com/developers/sdk/web-utm-tracking)
|
|
487
|
+
- [Tracking events](https://docs.joryio.com/developers/sdk/tracking-events)
|
|
488
|
+
- [E-commerce tracking](https://docs.joryio.com/developers/sdk/ecommerce-tracking)
|
|
489
|
+
- [Changelog](./CHANGELOG.md)
|
|
490
|
+
|
|
491
|
+
## Support
|
|
492
|
+
|
|
493
|
+
- Issues: https://github.com/joryio/joryio-web-sdk/issues
|
|
494
|
+
- Email: support@joryio.com
|
|
495
|
+
|
|
496
|
+
## License
|
|
497
|
+
|
|
498
|
+
Commercial - see [LICENSE](./LICENSE). Use of the SDK requires an active Joryio subscription or agreement.
|