Skip to content

Commit 27438f3

Browse files
feat: add demo page for end-to-end testing
Interactive page with dark/light mode toggle (onyx theme). Exercises every API method against local, dev, or sandbox backend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 862c03e commit 27438f3

1 file changed

Lines changed: 369 additions & 0 deletions

File tree

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Audience Web SDK — Demo</title>
7+
<style>
8+
:root {
9+
--bg: #050505; --card: #0f0f0f; --border: #1c1c1c; --accent: #888888;
10+
--text: #c0c0c0; --text-strong: #e8e8e8; --text-muted: #606060;
11+
--danger: #ef4444; --primary-bg: #e8e8e8; --primary-text: #050505;
12+
--primary-hover: #c0c0c0; --btn-bg: #1a1a1a; --btn-border: #2a2a2a;
13+
--btn-hover: #242424; --input-bg: #050505; --log-bg: #030303;
14+
--log-border: #141414; --log-time: #444444; --log-method: #d0d0d0;
15+
}
16+
[data-theme="light"] {
17+
--bg: #fafafa; --card: #ffffff; --border: #e4e4e7; --accent: #71717a;
18+
--text: #3f3f46; --text-strong: #18181b; --text-muted: #a1a1aa;
19+
--danger: #dc2626; --primary-bg: #18181b; --primary-text: #fafafa;
20+
--primary-hover: #3f3f46; --btn-bg: #f4f4f5; --btn-border: #d4d4d8;
21+
--btn-hover: #e4e4e7; --input-bg: #ffffff; --log-bg: #f4f4f5;
22+
--log-border: #e4e4e7; --log-time: #a1a1aa; --log-method: #18181b;
23+
}
24+
* { box-sizing: border-box; margin: 0; padding: 0; }
25+
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); padding: 32px 24px; max-width: 960px; margin: 0 auto; transition: background 0.2s, color 0.2s; }
26+
h1 { font-size: 22px; font-weight: 600; margin-bottom: 4px; color: var(--text-strong); letter-spacing: -0.3px; }
27+
.subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 28px; }
28+
.section { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 18px; margin-bottom: 14px; }
29+
.section h2 { font-size: 11px; color: var(--accent); margin-bottom: 14px; text-transform: uppercase; letter-spacing: 1.2px; font-weight: 600; }
30+
.row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
31+
button { background: var(--btn-bg); border: 1px solid var(--btn-border); color: var(--text); padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; transition: all 0.15s; }
32+
button:hover { background: var(--btn-hover); border-color: var(--accent); color: var(--text-strong); }
33+
button.primary { background: var(--primary-bg); border-color: var(--primary-bg); color: var(--primary-text); font-weight: 500; }
34+
button.primary:hover { background: var(--primary-hover); }
35+
button.danger { border-color: var(--danger); color: var(--danger); }
36+
button.danger:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }
37+
input, select { background: var(--input-bg); border: 1px solid var(--btn-border); color: var(--text); padding: 8px 10px; border-radius: 6px; font-size: 13px; font-family: inherit; transition: border-color 0.15s; }
38+
input:focus, select:focus { outline: none; border-color: var(--accent); }
39+
input { width: 200px; }
40+
select { min-width: 120px; }
41+
label { font-size: 11px; color: var(--text-muted); display: block; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
42+
.field { display: flex; flex-direction: column; }
43+
#log { background: var(--log-bg); border: 1px solid var(--border); border-radius: 8px; padding: 12px; font-size: 12px; line-height: 1.7; max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', monospace; }
44+
.log-entry { border-bottom: 1px solid var(--log-border); padding: 4px 0; }
45+
.log-time { color: var(--log-time); }
46+
.log-method { color: var(--log-method); font-weight: 600; }
47+
.log-ok { color: var(--accent); }
48+
.log-err { color: var(--danger); }
49+
.log-info { color: var(--accent); }
50+
.status-bar { display: flex; gap: 16px; font-size: 12px; padding: 10px 0; }
51+
.status-bar span { color: var(--text-muted); }
52+
.status-bar strong { color: var(--text-strong); }
53+
#consent-badge { padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
54+
.theme-toggle { position: fixed; top: 16px; right: 16px; background: var(--btn-bg); border: 1px solid var(--btn-border); color: var(--text); padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; font-family: inherit; z-index: 10; }
55+
.theme-toggle:hover { background: var(--btn-hover); border-color: var(--accent); }
56+
</style>
57+
<link rel="preconnect" href="https://fonts.googleapis.com">
58+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
59+
</head>
60+
<body>
61+
<button class="theme-toggle" onclick="toggleTheme()">Light / Dark</button>
62+
<h1>Audience Web SDK — Demo</h1>
63+
<p class="subtitle">End-to-end testing against dev/sandbox backend. Open DevTools Network tab to see requests.</p>
64+
65+
<div class="status-bar">
66+
<span>Environment: <strong id="env-display"></strong></span>
67+
<span>Consent: <span id="consent-badge"></span></span>
68+
<span>Anonymous ID: <strong id="anon-display"></strong></span>
69+
<span>User ID: <strong id="user-display">none</strong></span>
70+
</div>
71+
72+
<!-- Init -->
73+
<div class="section">
74+
<h2>1. Initialise</h2>
75+
<div class="row">
76+
<div class="field">
77+
<label>Publishable Key</label>
78+
<input id="pk" type="text" placeholder="pk_imtbl_..." value="">
79+
</div>
80+
<div class="field">
81+
<label>Environment</label>
82+
<select id="env">
83+
<option value="local">local (localhost:8070)</option>
84+
<option value="dev">dev</option>
85+
<option value="sandbox" selected>sandbox</option>
86+
<option value="production">production</option>
87+
</select>
88+
</div>
89+
<div class="field">
90+
<label>Initial Consent</label>
91+
<select id="init-consent">
92+
<option value="none">none</option>
93+
<option value="anonymous" selected>anonymous</option>
94+
<option value="full">full</option>
95+
</select>
96+
</div>
97+
</div>
98+
<div class="row">
99+
<label style="display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text)">
100+
<input type="checkbox" id="auto-page"> Auto track page views
101+
</label>
102+
<label style="display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text)">
103+
<input type="checkbox" id="debug-mode" checked> Debug mode
104+
</label>
105+
</div>
106+
<div class="row" style="margin-top:8px">
107+
<button class="primary" onclick="initSDK()">Init SDK</button>
108+
<button class="danger" onclick="shutdownSDK()">Shutdown</button>
109+
</div>
110+
</div>
111+
112+
<!-- Consent -->
113+
<div class="section">
114+
<h2>2. Consent</h2>
115+
<div class="row">
116+
<button onclick="setConsent('none')">Set: none</button>
117+
<button onclick="setConsent('anonymous')">Set: anonymous</button>
118+
<button onclick="setConsent('full')">Set: full</button>
119+
<button onclick="verifyConsent()" style="margin-left:16px">Verify Server Consent</button>
120+
</div>
121+
<div id="consent-verify" style="margin-top:8px;font-size:13px;display:none"></div>
122+
</div>
123+
124+
<!-- Page -->
125+
<div class="section">
126+
<h2>3. Page Tracking</h2>
127+
<div class="row">
128+
<button onclick="firePage()">page()</button>
129+
<button onclick="firePage({section:'shop',category:'weapons'})">page({section, category})</button>
130+
<button onclick="simulateSPA('/shop')">SPA → /shop</button>
131+
<button onclick="simulateSPA('/cart')">SPA → /cart</button>
132+
<button onclick="simulateSPA('/')">SPA → /</button>
133+
</div>
134+
</div>
135+
136+
<!-- Track -->
137+
<div class="section">
138+
<h2>4. Track Events</h2>
139+
<div class="row">
140+
<button onclick="trackEvent('sign_up', {method:'google'})">SignUp</button>
141+
<button onclick="trackEvent('sign_in', {method:'passport'})">SignIn</button>
142+
<button onclick="trackEvent('purchase', {currency:'USD',value:9.99,itemId:'sword_01',itemName:'Flame Sword',quantity:1})">Purchase</button>
143+
<button onclick="trackEvent('wishlist_add', {gameId:'game_123',source:'landing_page'})">WishlistAdd</button>
144+
<button onclick="trackEvent('session_start', {})">SessionStart</button>
145+
<button onclick="trackEvent('level_reached', {level:5,characterClass:'warrior'})">LevelReached</button>
146+
</div>
147+
<div class="row" style="margin-top:8px">
148+
<div class="field">
149+
<label>Custom Event</label>
150+
<input id="custom-event" type="text" placeholder="event_name" value="beta_key_redeemed">
151+
</div>
152+
<button onclick="trackCustom()" style="align-self:flex-end">Track Custom</button>
153+
</div>
154+
</div>
155+
156+
<!-- Identity -->
157+
<div class="section">
158+
<h2>5. Identity</h2>
159+
<div class="row">
160+
<div class="field">
161+
<label>User ID</label>
162+
<input id="uid" type="text" placeholder="user@example.com" value="user@example.com">
163+
</div>
164+
<div class="field">
165+
<label>Provider</label>
166+
<select id="provider">
167+
<option value="email">Email</option>
168+
<option value="steam">Steam</option>
169+
<option value="passport">Passport</option>
170+
<option value="epic">Epic</option>
171+
<option value="google">Google</option>
172+
<option value="apple">Apple</option>
173+
<option value="discord">Discord</option>
174+
<option value="custom">Custom</option>
175+
</select>
176+
</div>
177+
<button onclick="doIdentify()" style="align-self:flex-end" class="primary">identify()</button>
178+
</div>
179+
<div class="row" style="margin-top:8px">
180+
<button onclick="doAlias()">alias(Steam → Email)</button>
181+
<button onclick="doReset()" class="danger">reset()</button>
182+
</div>
183+
</div>
184+
185+
<!-- Flush -->
186+
<div class="section">
187+
<h2>6. Queue</h2>
188+
<div class="row">
189+
<button class="primary" onclick="doFlush()">Flush Now</button>
190+
</div>
191+
</div>
192+
193+
<!-- Log -->
194+
<div class="section">
195+
<h2>Event Log</h2>
196+
<div id="log"></div>
197+
<button onclick="document.getElementById('log').innerHTML=''" style="margin-top:8px">Clear Log</button>
198+
</div>
199+
200+
<script type="importmap">
201+
{ "imports": { "@imtbl/audience": "../../sdk/dist/browser/index.js" } }
202+
</script>
203+
<script type="module">
204+
import { ImmutableWebSDK, AudienceEvent, IdentityProvider } from '../dist/browser/index.js';
205+
206+
let sdk = null;
207+
208+
// Expose to onclick handlers
209+
window.ImmutableWebSDK = ImmutableWebSDK;
210+
window.AudienceEvent = AudienceEvent;
211+
window.IdentityProvider = IdentityProvider;
212+
213+
function log(method, detail, type = 'info') {
214+
const el = document.getElementById('log');
215+
const time = new Date().toISOString().slice(11, 23);
216+
const cls = type === 'ok' ? 'log-ok' : type === 'err' ? 'log-err' : 'log-info';
217+
const detailStr = typeof detail === 'object' ? JSON.stringify(detail, null, 2) : detail;
218+
el.innerHTML += `<div class="log-entry"><span class="log-time">${time}</span> <span class="log-method">${method}</span> <span class="${cls}">${detailStr}</span></div>`;
219+
el.scrollTop = el.scrollHeight;
220+
}
221+
222+
function updateStatus() {
223+
const anonCookie = document.cookie.match(/imtbl_anon_id=([^;]*)/);
224+
const consentCookie = document.cookie.match(/_imtbl_consent=([^;]*)/);
225+
document.getElementById('anon-display').textContent = anonCookie ? anonCookie[1].slice(0, 12) + '...' : 'none';
226+
const level = consentCookie ? consentCookie[1] : '—';
227+
const badge = document.getElementById('consent-badge');
228+
badge.textContent = level;
229+
const isDark = !document.documentElement.hasAttribute('data-theme');
230+
badge.style.background = level === 'full' ? (isDark ? '#e8e8e8' : '#18181b') : level === 'anonymous' ? '#888888' : level === 'none' ? 'var(--danger)' : '#444444';
231+
badge.style.color = level === 'full' ? (isDark ? '#050505' : '#fafafa') : level === 'anonymous' ? '#fff' : '#fff';
232+
badge.style.color = '#fff';
233+
}
234+
235+
window.initSDK = function() {
236+
if (sdk) { log('init', 'Already initialised — shutdown first', 'err'); return; }
237+
const pk = document.getElementById('pk').value.trim();
238+
if (!pk) { log('init', 'Enter a publishable key', 'err'); return; }
239+
const env = document.getElementById('env').value;
240+
const consent = document.getElementById('init-consent').value;
241+
const trackPageViews = document.getElementById('auto-page').checked;
242+
const debug = document.getElementById('debug-mode').checked;
243+
244+
sdk = ImmutableWebSDK.init({ publishableKey: pk, environment: env, consent, trackPageViews, debug });
245+
document.getElementById('env-display').textContent = env;
246+
log('init', { environment: env, consent, trackPageViews, debug }, 'ok');
247+
updateStatus();
248+
};
249+
250+
window.shutdownSDK = function() {
251+
if (!sdk) { log('shutdown', 'Not initialised', 'err'); return; }
252+
sdk.shutdown();
253+
sdk = null;
254+
document.getElementById('user-display').textContent = 'none';
255+
log('shutdown', 'SDK stopped', 'ok');
256+
};
257+
258+
window.setConsent = function(level) {
259+
if (!sdk) { log('setConsent', 'Init SDK first', 'err'); return; }
260+
sdk.setConsent(level);
261+
log('setConsent', level, 'ok');
262+
updateStatus();
263+
};
264+
265+
window.firePage = function(props) {
266+
if (!sdk) { log('page', 'Init SDK first', 'err'); return; }
267+
sdk.page(props);
268+
log('page', props || '(no properties)', 'ok');
269+
};
270+
271+
window.simulateSPA = function(path) {
272+
history.pushState({}, '', path);
273+
log('SPA', `pushState → ${path}`, 'info');
274+
};
275+
276+
window.trackEvent = function(event, properties) {
277+
if (!sdk) { log('track', 'Init SDK first', 'err'); return; }
278+
sdk.track(event, properties);
279+
log('track', { event, properties }, 'ok');
280+
};
281+
282+
window.trackCustom = function() {
283+
const name = document.getElementById('custom-event').value.trim();
284+
if (!sdk) { log('track', 'Init SDK first', 'err'); return; }
285+
if (!name) { log('track', 'Enter an event name', 'err'); return; }
286+
sdk.track(name, { source: 'demo' });
287+
log('track', { event: name, properties: { source: 'demo' } }, 'ok');
288+
};
289+
290+
window.doIdentify = function() {
291+
if (!sdk) { log('identify', 'Init SDK first', 'err'); return; }
292+
const uid = document.getElementById('uid').value.trim();
293+
const provider = document.getElementById('provider').value;
294+
if (!uid) { log('identify', 'Enter a user ID', 'err'); return; }
295+
sdk.identify(uid, provider, { name: 'Demo User' });
296+
document.getElementById('user-display').textContent = `${provider}:${uid}`;
297+
log('identify', { uid, provider }, 'ok');
298+
};
299+
300+
window.doAlias = function() {
301+
if (!sdk) { log('alias', 'Init SDK first', 'err'); return; }
302+
sdk.alias(
303+
{ uid: '76561198012345', provider: IdentityProvider.Steam },
304+
{ uid: 'user@example.com', provider: IdentityProvider.Email },
305+
);
306+
log('alias', { from: 'steam:76561198012345', to: 'email:user@example.com' }, 'ok');
307+
};
308+
309+
window.doReset = function() {
310+
if (!sdk) { log('reset', 'Init SDK first', 'err'); return; }
311+
sdk.reset();
312+
document.getElementById('user-display').textContent = 'none';
313+
log('reset', 'Identity cleared, new anonymousId', 'ok');
314+
updateStatus();
315+
};
316+
317+
window.doFlush = function() {
318+
if (!sdk) { log('flush', 'Init SDK first', 'err'); return; }
319+
sdk.flush().then(() => log('flush', 'Queue flushed', 'ok'));
320+
};
321+
322+
window.verifyConsent = function() {
323+
const pk = document.getElementById('pk').value.trim();
324+
const env = document.getElementById('env').value;
325+
if (!pk) { log('verify', 'Enter a publishable key', 'err'); return; }
326+
327+
const anonCookie = document.cookie.match(/imtbl_anon_id=([^;]*)/);
328+
if (!anonCookie) { log('verify', 'No anonymousId cookie — init SDK with consent first', 'err'); return; }
329+
const anonId = anonCookie[1];
330+
331+
const baseUrls = { local: 'http://localhost:8070', dev: 'https://api.dev.immutable.com', sandbox: 'https://api.sandbox.immutable.com', production: 'https://api.immutable.com' };
332+
const url = `${baseUrls[env]}/v1/audience/tracking-consent?anonymousId=${encodeURIComponent(anonId)}`;
333+
334+
const el = document.getElementById('consent-verify');
335+
el.style.display = 'block';
336+
el.innerHTML = '<span style="color:#8b949e">Fetching server consent...</span>';
337+
338+
fetch(url, { headers: { 'x-immutable-publishable-key': pk } })
339+
.then(res => {
340+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
341+
return res.json();
342+
})
343+
.then(data => {
344+
const isLight = document.documentElement.hasAttribute('data-theme');
345+
const color = data.status === 'full' ? (isLight ? '#18181b' : '#e8e8e8') : data.status === 'anonymous' ? '#888888' : data.status === 'none' ? '#ef4444' : '#888888';
346+
el.innerHTML = `Server consent for <strong>${anonId.slice(0, 12)}...</strong>: <strong style="color:${color}">${data.status}</strong>`;
347+
log('verify', { anonymousId: anonId, serverStatus: data.status }, 'ok');
348+
})
349+
.catch(err => {
350+
el.innerHTML = `<span style="color:#f85149">Failed: ${err.message}</span>`;
351+
log('verify', err.message, 'err');
352+
});
353+
};
354+
355+
window.toggleTheme = function() {
356+
const html = document.documentElement;
357+
if (html.hasAttribute('data-theme')) {
358+
html.removeAttribute('data-theme');
359+
} else {
360+
html.setAttribute('data-theme', 'light');
361+
}
362+
updateStatus();
363+
};
364+
365+
// Initial status
366+
updateStatus();
367+
</script>
368+
</body>
369+
</html>

0 commit comments

Comments
 (0)