@arsel.sa/web-sdk 1.0.0 → 1.2.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 CHANGED
@@ -5,7 +5,109 @@ versioning follows [Semantic Versioning](https://semver.org/).
5
5
 
6
6
  Breaking changes to the public surface wait for a major release, and are listed here explicitly.
7
7
 
8
- ## [Unreleased]
8
+ ## [1.2.0] — 2026-09-02
9
+
10
+ ### Changed
11
+
12
+ - **`screen()` now emits `arsel.screen_view`, not `arsel.screen`.** The Android and
13
+ iOS SDKs have always used `screen_view`, and the split meant a segment or in-app
14
+ rule keyed on one name silently never matched the other platform. Renamed here
15
+ rather than on mobile because two of the three SDKs already agreed, and because
16
+ `screen()` only shipped in 1.1.0 — the exposure is ten days wide. If you built a
17
+ segment or automation on `arsel.screen`, repoint it at `arsel.screen_view`.
18
+
19
+ ### Added
20
+
21
+ - **`arsel.app_installed`.** Emitted once per browser profile, on the first `init()` the SDK ever
22
+ runs there, ahead of the first `arsel.session_start`. Carries `sdk_version` and `platform`. A
23
+ browser has no install step, so this means "the first time we saw this device" — clearing site
24
+ data or opening a private window resets the store and re-fires it, and web install counts run
25
+ high by exactly that much.
26
+
27
+ **Browsers that already used an older version get no install event.** They are seeded silently on
28
+ their first load after the upgrade: emitting would have reported the whole existing audience as
29
+ installs on the day you shipped. Install-based segments start empty and fill forward.
30
+
31
+ ## [1.1.0] — 2026-08-23
32
+
33
+ ### Added
34
+
35
+ - **In-app messaging.** Messages authored in Arsel now render in the page, with no notification
36
+ permission and no prompt of any kind. The server resolves audience, consent, campaign window,
37
+ grants and lifetime caps into a per-device bundle; the SDK evaluates the trigger and the
38
+ session-scoped caps locally, so drawing a message costs no network round-trip. Four layouts —
39
+ `MODAL`, `BANNER_TOP`, `BANNER_BOTTOM`, `IMAGE_ONLY` — rendered into a closed shadow root with
40
+ a constructed stylesheet (falling back to `<style>`), text set with `textContent` only, and
41
+ colours applied through CSSOM so a strict `style-src` cannot strip them.
42
+ - **`screen(name, properties?)`.** Records a screen or page view. One request, two consumers: the
43
+ event reaches segments and automations exactly as `track()` would, and it is the trigger source
44
+ for `SCREEN_VIEW` in-app messages.
45
+ - **`suppressInAppMessages(boolean)`.** Holds messages back for the moments a host app knows are
46
+ wrong — a checkout step, a video playing full-screen.
47
+ - **`inApp` config option.** `true` by default; pass `false` to disable, or `{ zIndex, closeLabel }`
48
+ to tune the layer. The default `zIndex` sits below the maximum so a host site's own top-most
49
+ modal still wins.
50
+ - **Device registration without a push subscription.** `init()` now registers the browser so the
51
+ bundle fetch can authenticate. It shows no prompt, creates no subscription, and never touches the
52
+ service worker — gating in-app behind `promptForPush()` would have restricted the channel to the
53
+ minority who accept notifications.
54
+ - **Accessibility.** Modals get `role="dialog"`, `aria-modal`, a focus trap reading `root.activeElement`
55
+ (a closed root makes `document.activeElement` resolve to the host), focus capture and restore, and
56
+ `Escape` to dismiss. Banners get `role="status"` with `aria-live="polite"` and never steal focus.
57
+ Direction is read from the host page, not assumed.
58
+ - **`arsel_iam_sync` service-worker handling.** A reserved silent push refreshes the bundle and
59
+ renders nothing. Ships inert — nothing emits it yet — so refresh is driven by `init()`, tab
60
+ visibility and the bundle's own TTL.
61
+ - Diagnostics gain `inAppMessages`, `inAppBundleVersion`, `inAppFetchedAtMs` and
62
+ `pendingInAppBeacons`.
63
+ - **`diagnostics().configError`.** Reports why initialization was refused, readable before
64
+ initialization — which is precisely the state it describes.
65
+
66
+ ### Changed
67
+
68
+ - **`getJson()` returns the full `{ result, code, body }` envelope**, matching `post()`. The bundle
69
+ fetch has to branch on `304` *before* `classify()` sees it — `classify` maps 304 to `permanent`,
70
+ which would discard the cache on every successful revalidation.
71
+ - **The store's queue helpers take a leading queue name.** In-app beacons live in their own object
72
+ store: the events drain stops at the first retryable failure to preserve history order, so a
73
+ single stuck beacon sharing that queue would wedge the entire analytics pipeline behind it.
74
+ - IndexedDB schema version 1 → 2, adding the beacon store. The upgrade is guarded by store name, so
75
+ a browser installed at v1 gains only what it is missing.
76
+ - The service worker now closes its database handle after reading. It opens without a version, and
77
+ a connection left open makes the page's upgrade fire `blocked` and hang until the worker is
78
+ terminated.
79
+ - **`init()` no longer rejects on invalid configuration.** It is routinely called un-awaited, so the
80
+ rejection reached the page as an unhandled rejection the host could not catch — and an analytics
81
+ SDK must not break a page over its own misconfiguration. It logs, declines to start, and the
82
+ collecting calls then genuinely no-op: a refused SDK that still queued would grow IndexedDB
83
+ behind a flush that can never succeed. All three Arsel SDKs now behave identically here.
84
+ - The four configuration rules match the Android and iOS SDKs exactly, including the `pub_` prefix
85
+ check — the one that catches a secret API key pasted into page-readable source.
86
+
87
+ ### Fixed
88
+
89
+ - **An unreachable push service no longer escapes `init()`.** `pushManager.subscribe()` rejects
90
+ whenever the browser cannot reach its push service — a firewalled network, a captive portal,
91
+ private browsing, a Chromium build without push support. None are the caller's mistake and none
92
+ are feature-detectable. Both call sites now resolve to `null` like every other failure here.
93
+ - **`init()` no longer leaks an unhandled rejection from its background work.** It kicks off the
94
+ stranded-events flush and the push reconcile fire-and-forget, and neither carried a catch — so
95
+ anything they threw (a queue row that will not parse, an IndexedDB failure) surfaced on the host
96
+ page as a rejection it could not intercept. The same failure mode `init()` itself was hardened
97
+ against in this release, through a different door. Both now match the catch every other
98
+ fire-and-forget call site here already used.
99
+ - **A revoked device no longer reports as subscribed.** A durable opt-out answers the register call
100
+ with `200` and deliberately leaves the row `REVOKED`; reading only the HTTP status, `subscribe()`
101
+ returned `true`, and `isSubscribed()` asked the browser, which keeps its `PushSubscription`
102
+ across an opt-out. So `promptForPush()` reported success and an app would show "notifications on"
103
+ to someone who can never receive another push. The backend's status is now persisted and
104
+ consulted as `subscriptionStatus`, matching the Android SDK.
105
+
106
+ ### Requires
107
+
108
+ - Backend support for the in-app endpoints, CORS on `/api/v1/orgs/*/in-app/*`, `If-None-Match` in
109
+ the client-API allowed headers, and the org's `allowedOrigins` containing the customer's origin —
110
+ an empty allowlist matches nothing and rejects every visitor.
9
111
 
10
112
  ## [1.0.0] — 2026-08-17
11
113