feat(audience): interactive demo page and CDN bundle#2837
feat(audience): interactive demo page and CDN bundle#2837ImmutableJeffrey wants to merge 10 commits intomainfrom
Conversation
|
View your CI Pipeline Execution ↗ for commit 28d0c5c
☁️ 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.
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