@arsel.sa/web-sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/dist/arsel.js +1053 -329
- package/dist/arsel.js.map +1 -1
- package/dist/arsel.umd.cjs +107 -1
- package/dist/arsel.umd.cjs.map +1 -1
- package/dist/events.d.ts +2 -0
- package/dist/inapp-view.d.ts +16 -0
- package/dist/inapp.d.ts +83 -0
- package/dist/index.d.ts +20 -9
- package/dist/push.d.ts +16 -0
- package/dist/session.d.ts +5 -1
- package/dist/store.d.ts +30 -4
- package/dist/transport.d.ts +1 -1
- package/dist/types.d.ts +32 -0
- package/dist/version.d.ts +1 -1
- package/package.json +2 -1
- package/sw/arsel-sw.js +27 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import type { ArselConfig, ArselDiagnostics, ArselIdentity, EventProperties } from './types';
|
|
2
|
-
export type { ArselConfig, ArselDiagnostics, ArselIdentity, EventProperties, } from './types';
|
|
2
|
+
export type { ArselConfig, ArselDiagnostics, ArselIdentity, EventProperties, InAppOptions, } from './types';
|
|
3
3
|
export { SDK_VERSION } from './version';
|
|
4
|
-
/**
|
|
5
|
-
* Start the SDK. Idempotent — a second call returns the first one's promise, so
|
|
6
|
-
* a framework that mounts twice does not mint two identities.
|
|
7
|
-
*
|
|
8
|
-
* Deliberately does **not** request notification permission. A prompt on page
|
|
9
|
-
* load is what gets an origin permanently blocked by Chrome's abusive-
|
|
10
|
-
* notification heuristics; call {@link promptForPush} from a user gesture.
|
|
11
|
-
*/
|
|
12
4
|
export declare function init(config: ArselConfig): Promise<void>;
|
|
13
5
|
/**
|
|
14
6
|
* Record something the user did.
|
|
@@ -21,6 +13,23 @@ export declare function init(config: ArselConfig): Promise<void>;
|
|
|
21
13
|
* Names beginning `arsel.` are reserved for the SDK and ignored.
|
|
22
14
|
*/
|
|
23
15
|
export declare function track(name: string, properties?: EventProperties): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Record a screen or page view.
|
|
18
|
+
*
|
|
19
|
+
* One POST, two consumers: the event reaches segments and automations exactly
|
|
20
|
+
* as `track()` would, and it is the trigger source for SCREEN_VIEW in-app
|
|
21
|
+
* messages. Deliberately does NOT call `track()` — that would enqueue a second
|
|
22
|
+
* event and observe the trigger twice.
|
|
23
|
+
*/
|
|
24
|
+
export declare function screen(name: string, properties?: EventProperties): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Hold in-app messages back, or let them through again.
|
|
27
|
+
*
|
|
28
|
+
* For the moments a host app knows are wrong — a checkout step, a video
|
|
29
|
+
* playing full-screen. Not persisted: it describes the current page, not the
|
|
30
|
+
* device.
|
|
31
|
+
*/
|
|
32
|
+
export declare function suppressInAppMessages(suppressed: boolean): void;
|
|
24
33
|
/**
|
|
25
34
|
* The person using this browser, as an id you already have.
|
|
26
35
|
*
|
|
@@ -69,6 +78,8 @@ export declare function flushNow(): Promise<void>;
|
|
|
69
78
|
export declare const Arsel: {
|
|
70
79
|
init: typeof init;
|
|
71
80
|
track: typeof track;
|
|
81
|
+
screen: typeof screen;
|
|
82
|
+
suppressInAppMessages: typeof suppressInAppMessages;
|
|
72
83
|
identify: typeof identify;
|
|
73
84
|
reset: typeof reset;
|
|
74
85
|
optOut: typeof optOut;
|
package/dist/push.d.ts
CHANGED
|
@@ -37,3 +37,19 @@ export declare function optOut(): Promise<boolean>;
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function reconcile(): Promise<void>;
|
|
39
39
|
export declare function isSubscribed(): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Register this browser WITHOUT a push subscription.
|
|
42
|
+
*
|
|
43
|
+
* In-app messaging needs an `installationId` and a `deviceSecret` to
|
|
44
|
+
* authenticate its bundle fetch, and needs no notification permission at all.
|
|
45
|
+
* Gating that behind `promptForPush()` would restrict the whole channel to the
|
|
46
|
+
* minority who accept notifications — throwing away most of its reach.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately does NOT prompt, subscribe, or touch the service worker. The
|
|
49
|
+
* refusal to prompt on load is what keeps an origin out of Chrome's abusive-
|
|
50
|
+
* notification heuristics, and this path must not become a back door to it.
|
|
51
|
+
*
|
|
52
|
+
* Single-flighted: `promptForPush()` racing `init()` must not post twice, which
|
|
53
|
+
* would mint a second `deviceSecret` and strand the first.
|
|
54
|
+
*/
|
|
55
|
+
export declare function registerDevice(): Promise<boolean>;
|
package/dist/session.d.ts
CHANGED
|
@@ -18,4 +18,8 @@ export declare const SESSION_GAP_MS: number;
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function onVisible(now?: number): Promise<void>;
|
|
20
20
|
export declare function onHidden(now?: number): Promise<void>;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* @param onOpen invoked AFTER `onVisible()` resolves, so the session-start write
|
|
23
|
+
* has already landed and anything keyed on the session window reads the new one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function attach(onOpen?: () => void): void;
|
package/dist/store.d.ts
CHANGED
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
* unreachable from a worker. Splitting state across both stores would put the
|
|
8
8
|
* two halves of one fact in two places.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* The two durable queues.
|
|
12
|
+
*
|
|
13
|
+
* In-app beacons live in their OWN store, not alongside events. The events
|
|
14
|
+
* drain stops at the first retryable failure to preserve a user's history
|
|
15
|
+
* order — so a single stuck beacon sharing that queue would wedge the entire
|
|
16
|
+
* analytics pipeline behind it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const QUEUE: {
|
|
19
|
+
readonly events: "events";
|
|
20
|
+
readonly inAppBeacons: "inapp_beacons";
|
|
21
|
+
};
|
|
22
|
+
export type QueueStore = (typeof QUEUE)[keyof typeof QUEUE];
|
|
10
23
|
export interface PendingEvent {
|
|
11
24
|
/** Monotonic within a tab; the auto-increment key doubles as delivery order. */
|
|
12
25
|
id?: number;
|
|
@@ -21,10 +34,12 @@ export interface PendingEvent {
|
|
|
21
34
|
export declare function get<T>(key: string): Promise<T | null>;
|
|
22
35
|
export declare function set(key: string, value: unknown): Promise<unknown>;
|
|
23
36
|
export declare function remove(key: string): Promise<unknown>;
|
|
24
|
-
export declare function addEvent(body: string, idempotencyKey
|
|
25
|
-
export declare function allEvents(): Promise<PendingEvent[]>;
|
|
26
|
-
export declare function removeEvent(id: number): Promise<unknown>;
|
|
27
|
-
export declare function countEvents(): Promise<number>;
|
|
37
|
+
export declare function addEvent(store: QueueStore, body: string, idempotencyKey?: string): Promise<unknown>;
|
|
38
|
+
export declare function allEvents(store: QueueStore): Promise<PendingEvent[]>;
|
|
39
|
+
export declare function removeEvent(store: QueueStore, id: number): Promise<unknown>;
|
|
40
|
+
export declare function countEvents(store: QueueStore): Promise<number>;
|
|
41
|
+
/** Oldest-first trim, so an offline spell cannot grow the queue without bound. */
|
|
42
|
+
export declare function trimQueue(store: QueueStore, max: number): Promise<void>;
|
|
28
43
|
export declare const KEYS: {
|
|
29
44
|
readonly clientKey: "client_key";
|
|
30
45
|
readonly baseUrl: "base_url";
|
|
@@ -36,11 +51,22 @@ export declare const KEYS: {
|
|
|
36
51
|
readonly deviceSecret: "device_secret";
|
|
37
52
|
readonly vapidKeyVersion: "vapid_key_version";
|
|
38
53
|
readonly endpoint: "endpoint";
|
|
54
|
+
readonly subscriptionStatus: "subscription_status";
|
|
39
55
|
readonly sessionStartedAt: "session_started_at";
|
|
40
56
|
readonly backgroundedAt: "backgrounded_at";
|
|
41
57
|
readonly lastResponseCode: "last_response_code";
|
|
42
58
|
readonly lastResponsePath: "last_response_path";
|
|
43
59
|
readonly lastResponseAtMs: "last_response_at";
|
|
60
|
+
/** `{ bundleVersion, ttlSeconds, fetchedAtMs, messages }` — the cached bundle. */
|
|
61
|
+
readonly inAppBundle: "inapp_bundle";
|
|
62
|
+
/** messageId -> lifetime counters. Survives `reset()`: it describes the device. */
|
|
63
|
+
readonly inAppState: "inapp_state";
|
|
64
|
+
/**
|
|
65
|
+
* `{ startedAt, counts }`. Persisted rather than held in memory so
|
|
66
|
+
* `maxPerSession` survives a navigation — on a multi-page site an in-memory
|
|
67
|
+
* counter makes the cap per-page, which is not a cap at all.
|
|
68
|
+
*/
|
|
69
|
+
readonly inAppSession: "inapp_session";
|
|
44
70
|
};
|
|
45
71
|
/**
|
|
46
72
|
* Person-shaped identity, minted on first use. Distinct from the installation
|
package/dist/transport.d.ts
CHANGED
|
@@ -19,4 +19,4 @@ export declare const CODE_NO_RESPONSE = -1;
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function classify(code: number, authenticated: boolean): Result;
|
|
21
21
|
export declare function post<T = unknown>(baseUrl: string, path: string, body: unknown, headers?: Record<string, string>, authenticated?: boolean): Promise<Response<T>>;
|
|
22
|
-
export declare function getJson<T>(baseUrl: string, path: string): Promise<T
|
|
22
|
+
export declare function getJson<T>(baseUrl: string, path: string, headers?: Record<string, string>, authenticated?: boolean, init?: RequestInit): Promise<Response<T>>;
|
package/dist/types.d.ts
CHANGED
|
@@ -26,6 +26,21 @@ export interface ArselConfig {
|
|
|
26
26
|
serviceWorker?: 'external';
|
|
27
27
|
/** Emit SDK diagnostics to the console. Off by default. */
|
|
28
28
|
debug?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* In-app messaging. `true` (the default) registers this browser for in-app
|
|
31
|
+
* messages — which needs no notification permission and shows no prompt —
|
|
32
|
+
* and renders them. Pass `false` to disable, or an object to tune the layer.
|
|
33
|
+
*/
|
|
34
|
+
inApp?: boolean | InAppOptions;
|
|
35
|
+
}
|
|
36
|
+
export interface InAppOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Stacking context for the message host. Default 2147483000 — deliberately
|
|
39
|
+
* below the maximum, so a host site's own top-most modal still wins.
|
|
40
|
+
*/
|
|
41
|
+
zIndex?: number;
|
|
42
|
+
/** Accessible label for the close control. Default `'Close'`. */
|
|
43
|
+
closeLabel?: string;
|
|
29
44
|
}
|
|
30
45
|
/** Identifiers accepted by {@link identify}. Supply whichever you hold. */
|
|
31
46
|
export interface ArselIdentity {
|
|
@@ -53,6 +68,12 @@ export interface WebPushConfig {
|
|
|
53
68
|
export interface ArselDiagnostics {
|
|
54
69
|
sdkVersion: string;
|
|
55
70
|
initialized: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Why the SDK refused to start, or null. Set when init() was given an invalid
|
|
73
|
+
* configuration: nothing is collected and no call has any effect until it is
|
|
74
|
+
* fixed. Same field, same rules, on all three Arsel SDKs.
|
|
75
|
+
*/
|
|
76
|
+
configError: string | null;
|
|
56
77
|
anonymousId: string | null;
|
|
57
78
|
hasAssertedIdentity: boolean;
|
|
58
79
|
installationId: string | null;
|
|
@@ -61,8 +82,19 @@ export interface ArselDiagnostics {
|
|
|
61
82
|
pendingEvents: number;
|
|
62
83
|
permission: NotificationPermission | 'unsupported';
|
|
63
84
|
isSubscribed: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* The backend's last reported `status` for this device, e.g. `REVOKED` after a
|
|
87
|
+
* durable opt-out. Named as on the Android SDK. Null before the first register.
|
|
88
|
+
*/
|
|
89
|
+
subscriptionStatus: string | null;
|
|
64
90
|
vapidKeyVersion: number | null;
|
|
65
91
|
lastResponseCode: number | null;
|
|
66
92
|
lastResponsePath: string | null;
|
|
67
93
|
lastResponseAtMs: number | null;
|
|
94
|
+
/** Messages currently cached for this device. */
|
|
95
|
+
inAppMessages: number;
|
|
96
|
+
inAppBundleVersion: string | null;
|
|
97
|
+
inAppFetchedAtMs: number | null;
|
|
98
|
+
/** Beacons persisted but not yet delivered. A number that only grows is the tell. */
|
|
99
|
+
pendingInAppBeacons: number;
|
|
68
100
|
}
|
package/dist/version.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arsel.sa/web-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Arsel web SDK — events, identity and web push.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Arsel (https://arsel.sa)",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^22.10.0",
|
|
55
55
|
"fake-indexeddb": "^6.0.0",
|
|
56
|
+
"happy-dom": "^15.11.0",
|
|
56
57
|
"typescript": "^5.7.2",
|
|
57
58
|
"vite": "^6.0.0",
|
|
58
59
|
"vitest": "^2.1.8"
|
package/sw/arsel-sw.js
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
// Duplicated from src/ on purpose — this file is served as-is, uncompiled, and
|
|
17
17
|
// cannot import from the SDK build. Change a constant there, change it here.
|
|
18
|
-
const SDK_VERSION = '1.
|
|
18
|
+
const SDK_VERSION = '1.1.0';
|
|
19
19
|
const DB_NAME = 'arsel';
|
|
20
20
|
const KV_STORE = 'kv';
|
|
21
|
+
/** Reserved data key: refresh the in-app bundle, render nothing. */
|
|
22
|
+
const IAM_SYNC_KEY = 'arsel_iam_sync';
|
|
21
23
|
|
|
22
24
|
/** `showNotification` silently drops anything past this in Chrome. */
|
|
23
25
|
const MAX_ACTIONS = 2;
|
|
@@ -48,7 +50,14 @@ function openDb() {
|
|
|
48
50
|
|
|
49
51
|
async function readKeys(keys) {
|
|
50
52
|
const db = await openDb();
|
|
51
|
-
|
|
53
|
+
// Closed on every path below. openDb() opens WITHOUT a version, so a
|
|
54
|
+
// connection left open here holds the database at its current version and
|
|
55
|
+
// makes the page's next upgrade fire `blocked` — hanging until this worker is
|
|
56
|
+
// terminated.
|
|
57
|
+
if (!db.objectStoreNames.contains(KV_STORE)) {
|
|
58
|
+
db.close();
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
52
61
|
const store = db.transaction(KV_STORE, 'readonly').objectStore(KV_STORE);
|
|
53
62
|
const entries = await Promise.all(
|
|
54
63
|
keys.map(
|
|
@@ -60,6 +69,7 @@ async function readKeys(keys) {
|
|
|
60
69
|
}),
|
|
61
70
|
),
|
|
62
71
|
);
|
|
72
|
+
db.close();
|
|
63
73
|
return Object.fromEntries(entries);
|
|
64
74
|
}
|
|
65
75
|
|
|
@@ -132,6 +142,21 @@ self.addEventListener('push', (event) => {
|
|
|
132
142
|
} catch {
|
|
133
143
|
return; // not ours, and not parseable
|
|
134
144
|
}
|
|
145
|
+
// The in-app sync ping is checked BEFORE the claim test below: it carries no
|
|
146
|
+
// messageId and no title, so it must never reach showNotification and must
|
|
147
|
+
// never report a `delivered` engagement for a message that was never shown.
|
|
148
|
+
//
|
|
149
|
+
// Inert-but-ready — nothing on the backend emits this key yet. Bundle refresh
|
|
150
|
+
// is driven entirely by init, visibility and the bundle's own TTL.
|
|
151
|
+
if (data[IAM_SYNC_KEY]) {
|
|
152
|
+
event.waitUntil(
|
|
153
|
+
(async () => {
|
|
154
|
+
const clients = await self.clients.matchAll({ type: 'window' });
|
|
155
|
+
for (const client of clients) client.postMessage({ type: IAM_SYNC_KEY });
|
|
156
|
+
})(),
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
135
160
|
// Claimed on arsel_v, with arsel_mid as the fallback — the same test the
|
|
136
161
|
// Android parser applies. There is no marker key on the wire.
|
|
137
162
|
if (!data[WIRE.VERSION] && !data[WIRE.MESSAGE_ID]) return;
|