feat(audience): interactive demo page and CDN bundle#2837
feat(audience): interactive demo page and CDN bundle#2837ImmutableJeffrey wants to merge 20 commits into
Conversation
|
View your CI Pipeline Execution ↗ for commit 84d3e6b
☁️ Nx Cloud last updated this comment at |
7a073b4 to
7da2977
Compare
f354692 to
a92939b
Compare
nattb8
left a comment
There was a problem hiding this comment.
Shouldn't the demo live outside the sdk? Copy passport - audience/sdk-sample-app or audience/demo.
Also, this is included in the CDN bundle too. Don't think we want that.
Addresses review feedback on #2837 from @nattb8: the interactive demo should live in its own workspace package (matching the repo convention used by passport/sdk-sample-app, checkout/sdk-sample-app, dex/sdk-sample-app, bridge/bridge-sample-app) rather than inside the published @imtbl/audience package directory. Why this matters beyond aesthetics: - @imtbl/audience is a published npm package with a dedicated build pipeline (#2838): local tsup.config.js, prepack/postpack scripts that strip workspace deps from package.json, rollup-plugin-dts to inline type re-exports. The sdk package directory should stay focused on shipping artifacts; a demo harness is not one. - The demo was vanilla ES2020 (no TS, no modules, loaded via a script tag) while the sdk package is pure TypeScript. Co-locating them forced sdk/.eslintignore + an .eslintrc.cjs override block just to keep lint-staged from trying to parse demo/*.js with the TS parser. Both pieces of config disappear with this move. - The existing repo-wide root .eslintignore already has a `**sample-app**/` glob (for passport/sdk-sample-app and friends), so the new directory is automatically excluded from root lint with zero local config. Addresses the reviewer's secondary concern — "this is included in the CDN bundle too" — at the structural level. For the record, verified the demo was never literally bundled into dist/cdn/imtbl-audience.global.js: src/cdn.ts imports only ./sdk, ./config, and @imtbl/audience-core, and `files: ["dist"]` in package.json already excluded demo/ from the npm tarball. Confirmed by packing the sdk and inspecting the tarball — it only contains dist/browser, dist/cdn, dist/node, dist/types, plus README.md, LICENSE.md, and package.json. Changes: New package — packages/audience/sdk-sample-app/ - package.json: private, @imtbl/audience as a workspace:* devDep, engines node >= 20.11, `pnpm dev` builds @imtbl/audience then runs the local serve script - serve.mjs: ~90-line Node static server using only the stdlib. Serves the sample-app's own files from ./, and routes /vendor/ to ../sdk/dist/cdn/ so the HTML can load the CDN bundle via a same-origin URL (keeps the demo's CSP happy). Blocks serve.mjs, package.json, and node_modules from being served, plus path traversal attempts via decodeURIComponent + a resolve/startsWith guard. Verified with curl: 200 for /, /demo.css, /demo.js and /vendor/imtbl-audience.global.js(.map); 403 for /package.json, /serve.mjs, /vendor/../../package.json, /%2e%2e/secret; 404 for /nonexistent.html. - index.html, demo.js, demo.css, README.md: git-renamed from packages/audience/sdk/demo/. The only content change is in index.html — the <script src> moved from ../dist/cdn/... to vendor/... — plus README.md was updated with the new run instructions and a layout diagram for the new location. Package cleanup — packages/audience/sdk/ - Remove the `demo` script from package.json (its entry point is gone now). - Revert .eslintrc.cjs to main's 6-line baseline by dropping the 22-line `demo/**/*.js` overrides block that the PR had added. - Delete .eslintignore entirely (its only line was `demo/`). - Update README.md's two `demo/` references to point at `../sdk-sample-app/README.md` instead. Repo-level - Drop the `packages/audience/sdk/demo/` line from root .eslintignore (the existing `**sample-app**/` glob covers the new location). - Register `packages/audience/sdk-sample-app` in pnpm-workspace.yaml. - pnpm-lock.yaml picks up a 6-line importer entry for the new package (just the workspace:* link to ../sdk, no external deps). Verification: - `pnpm --filter @imtbl/audience-core --filter @imtbl/audience run lint typecheck test` — 113 core + 51 sdk tests pass, lint/typecheck clean on both packages. - `pnpm --filter @imtbl/audience run build` — ESM (browser+node), CDN IIFE (52.04 KB), and rolled-up .d.ts all build clean. - `pnpm --filter @imtbl/audience-sdk-sample-app run dev` — builds the sdk, starts the local server, demo loads at http://localhost:3456/ with the CDN bundle served from /vendor/. - `pnpm pack --pack-destination /tmp/...` in the sdk — tarball contains only dist/{browser,cdn,node,types}, LICENSE.md, README.md, and package.json. No demo, no vendor, no sample-app, no scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
a92939b to
6f121bc
Compare
Adds a self-contained IIFE bundle of @imtbl/audience so studios can load the SDK via a <script> tag without a build step. The bundle inlines @imtbl/audience-core and exposes the Audience class on window.ImmutableAudience. Build config (tsup.cdn.js): - IIFE format, minified, no externals - define: replaces __SDK_VERSION__ at build time with the version from package.json so the runtime can surface it (e.g. in debug logs and the demo footer) - cdn.ts entry exports SDK_VERSION and re-exports Audience/types package.json adds the 'build:cdn' script and a 'demo' script that builds then serves packages/audience/sdk on :3456. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds packages/audience/sdk/demo/: single-page interactive harness that loads the CDN bundle and exercises the Audience class. Initial surface: - index.html with Setup panel (publishable key, environment, initial consent), Init + Shutdown buttons, status bar, event log - demo.js wires Init -> Audience.init() and Shutdown -> audience.shutdown(), writes every SDK action to the event log with timestamps - demo.css with a minimal baseline layout Serves under `pnpm demo` from the package root at /demo/index.html. Later commits add the rest of the public method buttons, styling polish, and layout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires every public method on the Audience class to a button in the demo, one section per method: - Consent: three buttons (none, anonymous, full) call setConsent() - Page: button + properties textarea -> page(props) - Track: event name input + properties textarea -> track(name, props) - Identify: ID input + identityType select + traits textarea -> identify(id, type, traits); separate 'Identify (traits only)' button for the anonymous-visitor overload - Alias: from/to ID inputs + identityType selects -> alias(from, to) - Reset: reset() - Flush: flush() Every action writes a log entry with the method name and the payload that was sent. JSON parse errors on properties/traits inputs are caught and surfaced in the log rather than thrown. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Disable Init until the publishable key input has non-whitespace content (prevents calling Audience.init with an empty key). - Colour-code the consent badge in the status bar by level: red for none, amber for anonymous, green for full. Readable at a glance. - Add flushInterval and flushSize number inputs to the Setup panel so you can exercise non-default queue timings from the demo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On wide viewports the demo splits into two columns: controls on the left, event log on the right, with a drag gutter between them to adjust the split ratio. On narrow viewports the columns stack. The event log itself is also resizable on both wide and narrow layouts so you can see more history without scrolling. Keyboard users can resize the gutter with arrow keys (role="separator", aria-orientation, tabindex). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restyles the demo to match the light theme used by the passport SDK sample app at https://github.com/immutable/passport-sample-app so the two demos feel like a consistent family. Changes: - Light theme as the default (no dark mode toggle — see passport sample app for the design reference) - Typography refined: passport sample app font stack, adjusted sizes and line-heights for readability - Elevation applied to panels with subtle shadows and borders - Primary button colour matched to the passport sample app (no more cyan accent) - Input styling (including number inputs) normalised across the Setup panel and the method panels - Header subtitle removed — redundant given the page title Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Final UX polish for the demo: Footer and accessibility - Footer renders the SDK version (read from SDK_VERSION exported from cdn.ts — adds the const + a cdn.test.ts for the guard) - Event log marked with aria-live="polite" so screen readers announce new entries Event log - Copy button copies the full session's log to the clipboard (named just 'Copy' — clearer than a longer label) - Auto-scroll to bottom on new entries, but only while the user is already at the bottom — if they scroll up to inspect older events, auto-scroll locks so the view doesn't jump away Alias validation - Real-time check on the Alias button: disabled while either ID is empty or (fromId, fromType) === (toId, toType). Mirrors core's isAliasValid() so the user gets immediate feedback instead of discovering the problem after clicking Cleanup - Remove dead #panel-slot selectors from demo.css and the empty <div id="panel-slot"> from index.html (never used) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds two READMEs: packages/audience/sdk/README.md Package-level usage doc. Install (npm + CDN), quick start example, public method list with short signatures, consent level behaviour table, auto-tracked event list, cookie reference, and a pointer to the demo. packages/audience/sdk/demo/README.md Demo harness usage doc. How to run (pnpm demo → serves localhost:3456), test publishable keys for dev + sandbox, a step-by-step 'what to try' script, environments table, troubleshooting for the common issues (bundle failing to load, 400/403 from the API, no BigQuery data), and a files layout section. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Locks the demo's Content-Security-Policy to the minimum needed to run — audience API endpoints only, nothing else. - default-src 'self' - script-src 'self' (no inline scripts, no eval) - style-src 'self' (no inline styles) - connect-src limited to https://api.dev.immutable.com and https://api.sandbox.immutable.com Explicitly NOT in connect-src: api.immutable.com. The @imtbl/metrics SDK bundled into the CDN posts its own telemetry there, and those calls will be blocked by the browser with a CSP violation log. That is intentional — the demo is a harness, not a product, and the metrics bundle travelling along with the audience SDK shouldn't phone home from a localhost demo page. The violations do not affect demo behaviour; the audience calls still succeed. README's Security section explains this so the CSP violation lines in the console aren't mistaken for a bug. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses review feedback on #2837 from @nattb8: the interactive demo should live in its own workspace package (matching the repo convention used by passport/sdk-sample-app, checkout/sdk-sample-app, dex/sdk-sample-app, bridge/bridge-sample-app) rather than inside the published @imtbl/audience package directory. Why this matters beyond aesthetics: - @imtbl/audience is a published npm package with a dedicated build pipeline (#2838): local tsup.config.js, prepack/postpack scripts that strip workspace deps from package.json, rollup-plugin-dts to inline type re-exports. The sdk package directory should stay focused on shipping artifacts; a demo harness is not one. - The demo was vanilla ES2020 (no TS, no modules, loaded via a script tag) while the sdk package is pure TypeScript. Co-locating them forced sdk/.eslintignore + an .eslintrc.cjs override block just to keep lint-staged from trying to parse demo/*.js with the TS parser. Both pieces of config disappear with this move. - The existing repo-wide root .eslintignore already has a `**sample-app**/` glob (for passport/sdk-sample-app and friends), so the new directory is automatically excluded from root lint with zero local config. Addresses the reviewer's secondary concern — "this is included in the CDN bundle too" — at the structural level. For the record, verified the demo was never literally bundled into dist/cdn/imtbl-audience.global.js: src/cdn.ts imports only ./sdk, ./config, and @imtbl/audience-core, and `files: ["dist"]` in package.json already excluded demo/ from the npm tarball. Confirmed by packing the sdk and inspecting the tarball — it only contains dist/browser, dist/cdn, dist/node, dist/types, plus README.md, LICENSE.md, and package.json. Changes: New package — packages/audience/sdk-sample-app/ - package.json: private, @imtbl/audience as a workspace:* devDep, engines node >= 20.11, `pnpm dev` builds @imtbl/audience then runs the local serve script - serve.mjs: ~90-line Node static server using only the stdlib. Serves the sample-app's own files from ./, and routes /vendor/ to ../sdk/dist/cdn/ so the HTML can load the CDN bundle via a same-origin URL (keeps the demo's CSP happy). Blocks serve.mjs, package.json, and node_modules from being served, plus path traversal attempts via decodeURIComponent + a resolve/startsWith guard. Verified with curl: 200 for /, /demo.css, /demo.js and /vendor/imtbl-audience.global.js(.map); 403 for /package.json, /serve.mjs, /vendor/../../package.json, /%2e%2e/secret; 404 for /nonexistent.html. - index.html, demo.js, demo.css, README.md: git-renamed from packages/audience/sdk/demo/. The only content change is in index.html — the <script src> moved from ../dist/cdn/... to vendor/... — plus README.md was updated with the new run instructions and a layout diagram for the new location. Package cleanup — packages/audience/sdk/ - Remove the `demo` script from package.json (its entry point is gone now). - Revert .eslintrc.cjs to main's 6-line baseline by dropping the 22-line `demo/**/*.js` overrides block that the PR had added. - Delete .eslintignore entirely (its only line was `demo/`). - Update README.md's two `demo/` references to point at `../sdk-sample-app/README.md` instead. Repo-level - Drop the `packages/audience/sdk/demo/` line from root .eslintignore (the existing `**sample-app**/` glob covers the new location). - Register `packages/audience/sdk-sample-app` in pnpm-workspace.yaml. - pnpm-lock.yaml picks up a 6-line importer entry for the new package (just the workspace:* link to ../sdk, no external deps). Verification: - `pnpm --filter @imtbl/audience-core --filter @imtbl/audience run lint typecheck test` — 113 core + 51 sdk tests pass, lint/typecheck clean on both packages. - `pnpm --filter @imtbl/audience run build` — ESM (browser+node), CDN IIFE (52.04 KB), and rolled-up .d.ts all build clean. - `pnpm --filter @imtbl/audience-sdk-sample-app run dev` — builds the sdk, starts the local server, demo loads at http://localhost:3456/ with the CDN bundle served from /vendor/. - `pnpm pack --pack-destination /tmp/...` in the sdk — tarball contains only dist/{browser,cdn,node,types}, LICENSE.md, README.md, and package.json. No demo, no vendor, no sample-app, no scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6f121bc to
28d0c5c
Compare
| expect(warn).toHaveBeenCalledWith(expect.stringContaining('loaded twice')); | ||
| warn.mockRestore(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The as any cast is needed because the inline type shapes AudienceError as typeof Error. If you type it as typeof AudienceError instead, you can construct it without the cast and the compiler will catch a broken constructor signature.
| AudienceError: typeof Error; | ||
| IdentityType: Record<string, string>; | ||
| version: string; | ||
| }; |
There was a problem hiding this comment.
Using typeof Audience (imported from ./sdk) here would turn this into a compile-time check that the CDN export shape matches the real class.
… manager The README and demo page document onError as a config option, and the core infrastructure (MessageQueue.options.onError, createConsentManager's onError parameter, invokeOnError helper) already exists — but the web SDK's Audience class never passed the callback through. Any studio following the quickstart code would get a silently ignored handler. Adds onError to AudienceConfig and threads it into both MessageQueue options and createConsentManager so flush and consent-sync failures actually reach studio error handlers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses nattb8's two inline review comments on cdn.test.ts:
1. Replace the loose inline type shape ({ Audience: { init: Function },
AudienceError: typeof Error, IdentityType: Record<string, string> })
with a GlobalShape using typeof Audience, typeof AudienceError, and
typeof IdentityType from their real modules. If the CDN global shape
drifts from the actual SDK classes, the test now fails at compile
time instead of silently passing at runtime.
2. Remove the `as any` cast on `new g!.AudienceError(...)`. With the
real constructor type, TypeScript verifies the init object shape
matches what AudienceError actually accepts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ackage
The CDN bundle already exposes AudienceError on the global, and the
README documents it under "Error handling", but the npm package's
index.ts didn't re-export it. TypeScript users couldn't type their
onError callbacks without reaching into @imtbl/audience-core directly.
Adds AudienceError (class) and AudienceErrorCode (type union) to the
public exports so `import { AudienceError } from '@imtbl/audience'`
works.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds four tests that verify the onError callback wired in the previous commit actually reaches studio code end-to-end through httpSend: - flush failure (500) delivers FLUSH_FAILED - consent sync failure (503) delivers CONSENT_SYNC_FAILED - successful operations do not fire onError - exceptions thrown by the callback are swallowed (SDK keeps running) The core layer already tests the downstream machinery (toAudienceError, invokeOnError, queue retry semantics). These tests close the remaining gap: proving that Audience.init threads the callback into both the queue and the consent manager so it actually fires in a real SDK session. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds AudienceEvents — a single const object that defines every standard event name — mirroring the pattern Play uses in its audience-sdk/events module. Studios get autocomplete, typo protection, and a consistent schema instead of scattering raw strings across their codebase. The object includes SDK-managed session events (imported from core so the strings stay in sync) and recommended studio events carried over from the Play integration: email_acquired, game_page_viewed, link_clicked, sign_in, wishlist_add, wishlist_remove. Each event has a typed property interface (EmailAcquiredProperties, LinkClickedProperties, etc.) so studios can optionally type-check the properties they pass to track(). sdk.ts now imports session constants from ./events instead of directly from @imtbl/audience-core, making events.ts the single source of truth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The demo footer links to ./README.md but the MIME map didn't include .md, so the browser downloaded it as application/octet-stream instead of rendering it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ../README.md link resolved outside the serve root, so serve.mjs returned 403. The demo README link still works. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds an example of using the predefined event name constants alongside custom event strings, matching how Play uses the SDK. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
One-click buttons for each recommended event (game_page_viewed, link_clicked, email_acquired, sign_in, wishlist_add, wishlist_remove) with realistic demo properties matching the Play integration. Shared game ID input lets you vary the gameId across events. Walkthrough updated to cover the new panel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Renames the local GlobalShape to ImmutableAudienceGlobal, exports it from cdn.ts as a type-only export, and imports it in cdn.test.ts. Eliminates the duplicated definition that previously had to be kept in sync by hand. The runtime behaviour is unchanged — types are erased at compile time so cdn.ts remains a side-effect-only module for value imports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
What this PR does
Adds two things needed to ship
@imtbl/audiencefor studios that embed it with a<script>tag:dist/cdn/imtbl-audience.global.js. Studios drop one<script>tag into their HTML, then useImmutableAudience.Audience.init({...})to start tracking. No bundler, nonpm install.Audienceclass against the real dev / sandbox backend. Live event log, one button per method, test keys baked in. Fastest way to sanity-check SDK changes end-to-end.How to try it
cd packages/audience/sdk-sample-app pnpm devOpen http://localhost:3456/. Test publishable keys and a 10-step "what to try" walkthrough are in the sample-app's README.
Addressing the review feedback
@nattb8 asked two things in their review:
1. Demo moved out of the sdk package. It now lives in its own private workspace package at
packages/audience/sdk-sample-app, matching thepassport-sdk-sample-app/checkout-sdk-sample-app/dex-sdk-sample-appconvention. Thesdkpackage is now just the shippable library — no demo files mixed in with the build artifacts.2. Demo was never bundled into the single-file SDK. Verified this before doing the restructure:
src/cdn.ts(the entry point for the single-file build) imports only./sdk,./config, and@imtbl/audience-core— nothing in the TypeScript source references the demo's HTML/JS/CSS, so the minified output doesn't contain any of it. And the sdk's"files": ["dist"]field already excluded the demo directory from the published npm tarball. Confirmed by runningpnpm packon the sdk package and listing the archive contents — it has onlydist/{browser,cdn,node,types},LICENSE.md,README.md, andpackage.json. No demo files, no sample-app files. The restructure in point 1 removes any ambiguity at the directory level regardless.What changed at a glance
The branch is rebased onto current main, so it sits on top of #2838 (which introduced the
@imtbl/audiencepublishing pipeline). The two build systems coexist cleanly: the existingtsup.config.jsbuildsdist/browser+dist/nodeand the newtsup.cdn.jsbuildsdist/cdn, without either touching the other's output.Ten commits total — follow them in order if you want to see how the demo came together incrementally.
Related tickets
Test plan
pnpm --filter @imtbl/audience-core --filter @imtbl/audience run lint— clean on both packagesrun typecheck— cleanrun test— 113 core tests + 51 sdk tests all passpnpm --filter @imtbl/audience run build— produces all expected artifacts: browser ESM, node CJS+ESM, single-file web build (52 KB), and the rolled-up.d.tspnpm --filter @imtbl/audience-sdk-sample-app run dev— demo loads at http://localhost:3456/, every method exercisable, 200 responses from the sandbox audience APIpnpm packonpackages/audience/sdk— tarball is clean: only shipping artifacts, no demo files or sample-app files