@pixelume/sdk 0.4.0 → 0.6.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +62 -1
  2. package/README.md +97 -19
  3. package/dist/core/PixelumeClient.d.ts +10 -2
  4. package/dist/core/PixelumeClient.js +20 -2
  5. package/dist/core/PixelumeClient.js.map +1 -1
  6. package/dist/core/config.d.ts +18 -1
  7. package/dist/core/config.js +99 -0
  8. package/dist/core/config.js.map +1 -1
  9. package/dist/core/errors.d.ts +5 -0
  10. package/dist/core/errors.js.map +1 -1
  11. package/dist/core/types.d.ts +78 -0
  12. package/dist/core/types.js.map +1 -1
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/modules/analytics/AnalyticsModule.d.ts +93 -0
  16. package/dist/modules/analytics/AnalyticsModule.js +461 -0
  17. package/dist/modules/analytics/AnalyticsModule.js.map +1 -0
  18. package/dist/modules/analytics/AnalyticsProvider.d.ts +51 -0
  19. package/dist/modules/analytics/AnalyticsProvider.js +23 -0
  20. package/dist/modules/analytics/AnalyticsProvider.js.map +1 -0
  21. package/dist/modules/analytics/PostHogAnalyticsProvider.d.ts +39 -0
  22. package/dist/modules/analytics/PostHogAnalyticsProvider.js +255 -0
  23. package/dist/modules/analytics/PostHogAnalyticsProvider.js.map +1 -0
  24. package/dist/modules/analytics/events.d.ts +30 -0
  25. package/dist/modules/analytics/events.js +153 -0
  26. package/dist/modules/analytics/events.js.map +1 -0
  27. package/dist/modules/auth/AuthModule.d.ts +20 -1
  28. package/dist/modules/auth/AuthModule.js +305 -21
  29. package/dist/modules/auth/AuthModule.js.map +1 -1
  30. package/dist/ui/widget.js +15 -5
  31. package/dist/ui/widget.js.map +1 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/dist/version.js.map +1 -1
  35. package/package.json +4 -1
@@ -0,0 +1,461 @@
1
+ import { SDK_VERSION } from '../../version.js';
2
+ import { NoopAnalyticsProvider, } from './AnalyticsProvider.js';
3
+ import { ANALYTICS_EVENTS, isCatalogEvent, isValidEventName, normalizeLevel, normalizeReason, sanitizeProperties, } from './events.js';
4
+ import { PostHogAnalyticsProvider } from './PostHogAnalyticsProvider.js';
5
+ /** Events kept while the provider loads after consent; older ones are kept, newer dropped. */
6
+ export const MAX_PENDING_EVENTS = 100;
7
+ const MAX_TRACKED_LEVELS = 50;
8
+ const CONSENT_VERSION = 1;
9
+ /** Anonymous reasons meaning "the account is no longer the one playing". */
10
+ const SEPARATING_REASONS = new Set(['logged_out', 'expired', 'session_ended']);
11
+ /** Keys set by the SDK for an event, protected even when the SDK does not set them this time. */
12
+ const NO_PROTECTED_KEYS = new Set();
13
+ const SESSION_END_KEYS = new Set(['duration_ms', 'visible_duration_ms']);
14
+ const LEVEL_KEYS = new Set(['level', 'duration_ms']);
15
+ const LEVEL_FAILED_KEYS = new Set(['level', 'duration_ms', 'reason']);
16
+ export class AnalyticsModule {
17
+ #config;
18
+ #provider;
19
+ #browser;
20
+ #visibility;
21
+ #now;
22
+ #unsubscribeAuth;
23
+ #consent = 'unknown';
24
+ #status;
25
+ #pending = [];
26
+ #identity = null;
27
+ #session = null;
28
+ #loading = false;
29
+ #loaded = false;
30
+ /** Consent was revoked while the provider was loading: wipe it as soon as it is ready. */
31
+ #revokedWhileLoading = false;
32
+ #destroyed = false;
33
+ constructor(options) {
34
+ this.#config = options.config;
35
+ this.#browser = options.browser;
36
+ this.#visibility =
37
+ options.visibility === undefined ? browserVisibilitySource() : options.visibility;
38
+ this.#now = options.now ?? (() => Date.now());
39
+ const config = this.#config;
40
+ if (config === null) {
41
+ this.#provider = new NoopAnalyticsProvider();
42
+ this.#status = 'disabled';
43
+ this.#unsubscribeAuth = () => undefined;
44
+ return;
45
+ }
46
+ this.#provider = (options.createProvider ?? createDefaultProvider)(config);
47
+ this.#status = 'waiting_consent';
48
+ this.#identity = identityFor(options.auth.state);
49
+ this.#unsubscribeAuth = options.auth.subscribe((state) => {
50
+ this.#onAuthState(state);
51
+ });
52
+ const remembered = config.rememberConsent ? this.#readConsent() : null;
53
+ if (remembered === false)
54
+ this.#consent = 'denied';
55
+ if (remembered === true) {
56
+ this.#consent = 'granted';
57
+ // Asynchronous: constructing the client never waits for, nor fails because of, the provider.
58
+ this.#activate();
59
+ }
60
+ }
61
+ get consent() {
62
+ return this.#consent;
63
+ }
64
+ get status() {
65
+ return this.#status;
66
+ }
67
+ get sessionId() {
68
+ return this.#session?.id ?? null;
69
+ }
70
+ isEnabled() {
71
+ return this.#status === 'loading' || this.#status === 'ready';
72
+ }
73
+ setConsent(granted) {
74
+ const value = granted;
75
+ if (this.#destroyed || this.#config === null || typeof value !== 'boolean')
76
+ return;
77
+ const next = value ? 'granted' : 'denied';
78
+ // Repeating the decision is a no-op, except to retry a provider that could not load.
79
+ if (next === this.#consent && this.#status !== 'unavailable')
80
+ return;
81
+ this.#consent = next;
82
+ if (this.#config.rememberConsent)
83
+ this.#writeConsent(value);
84
+ if (value) {
85
+ this.#activate();
86
+ return;
87
+ }
88
+ this.#pending = [];
89
+ this.#status = 'waiting_consent';
90
+ if (this.#loading)
91
+ this.#revokedWhileLoading = true;
92
+ this.#guard(() => {
93
+ this.#provider.optOut();
94
+ });
95
+ }
96
+ track(event, properties) {
97
+ if (!isValidEventName(event)) {
98
+ this.#report('invalid_event', null);
99
+ return;
100
+ }
101
+ if (isCatalogEvent(event)) {
102
+ this.#report('reserved_event', event);
103
+ return;
104
+ }
105
+ this.#emit(event, properties, {});
106
+ }
107
+ startSession(properties) {
108
+ if (this.#destroyed || this.#session !== null)
109
+ return;
110
+ const now = this.#now();
111
+ // A no-op client never touches the page, not even to listen.
112
+ const visibility = this.#config === null ? null : this.#visibility;
113
+ const session = {
114
+ id: randomId(this.#browser),
115
+ startedAt: now,
116
+ visibleMs: 0,
117
+ visibleSince: visibility === null || visibility.isVisible() ? now : null,
118
+ unsubscribeVisibility: visibility?.subscribe(() => {
119
+ this.#onVisibilityChange(session);
120
+ }) ?? null,
121
+ levels: new Map(),
122
+ };
123
+ this.#session = session;
124
+ this.#emit(ANALYTICS_EVENTS.sessionStarted, properties, {});
125
+ }
126
+ endSession(properties) {
127
+ const session = this.#session;
128
+ if (session === null)
129
+ return;
130
+ const now = this.#now();
131
+ const visibleMs = session.visibleMs + (session.visibleSince === null ? 0 : now - session.visibleSince);
132
+ this.#emit(ANALYTICS_EVENTS.sessionEnded, properties, {
133
+ duration_ms: Math.max(0, Math.round(now - session.startedAt)),
134
+ visible_duration_ms: Math.max(0, Math.round(visibleMs)),
135
+ }, SESSION_END_KEYS);
136
+ this.#closeSession();
137
+ }
138
+ levelStarted(level, properties) {
139
+ const normalized = this.#level(ANALYTICS_EVENTS.levelStarted, level);
140
+ if (normalized === null)
141
+ return;
142
+ const levels = this.#session?.levels;
143
+ if (levels !== undefined) {
144
+ levels.delete(normalized);
145
+ if (levels.size >= MAX_TRACKED_LEVELS) {
146
+ const oldest = levels.keys().next();
147
+ if (oldest.done !== true)
148
+ levels.delete(oldest.value);
149
+ }
150
+ levels.set(normalized, this.#now());
151
+ }
152
+ this.#emit(ANALYTICS_EVENTS.levelStarted, properties, { level: normalized }, LEVEL_KEYS);
153
+ }
154
+ levelCompleted(level, properties) {
155
+ const normalized = this.#level(ANALYTICS_EVENTS.levelCompleted, level);
156
+ if (normalized === null)
157
+ return;
158
+ this.#emit(ANALYTICS_EVENTS.levelCompleted, properties, { level: normalized, ...this.#levelDuration(normalized) }, LEVEL_KEYS);
159
+ }
160
+ levelFailed(level, reason, properties) {
161
+ const normalized = this.#level(ANALYTICS_EVENTS.levelFailed, level);
162
+ if (normalized === null)
163
+ return;
164
+ const extra = { level: normalized, ...this.#levelDuration(normalized) };
165
+ if (reason !== undefined) {
166
+ const value = normalizeReason(reason);
167
+ if (value === null)
168
+ this.#report('invalid_property', ANALYTICS_EVENTS.levelFailed, 'reason');
169
+ else
170
+ extra.reason = value;
171
+ }
172
+ this.#emit(ANALYTICS_EVENTS.levelFailed, properties, extra, LEVEL_FAILED_KEYS);
173
+ }
174
+ /**
175
+ * @internal Called by `PixelumeClient.destroy()`: releases listeners and stops delivery. Does not
176
+ * end the game session nor change consent; posthog may still flush what it already accepted.
177
+ */
178
+ dispose() {
179
+ if (this.#destroyed)
180
+ return;
181
+ this.#destroyed = true;
182
+ this.#unsubscribeAuth();
183
+ this.#closeSession();
184
+ this.#pending = [];
185
+ this.#status = 'disabled';
186
+ this.#guard(() => {
187
+ this.#provider.detach();
188
+ });
189
+ }
190
+ #activate() {
191
+ if (this.#destroyed || this.#consent !== 'granted')
192
+ return;
193
+ if (this.#loading) {
194
+ this.#status = 'loading';
195
+ return;
196
+ }
197
+ if (this.#loaded) {
198
+ this.#status = 'ready';
199
+ this.#guard(() => {
200
+ this.#provider.optIn();
201
+ });
202
+ this.#applyIdentity();
203
+ return;
204
+ }
205
+ this.#status = 'loading';
206
+ this.#loading = true;
207
+ let loading;
208
+ try {
209
+ loading = this.#provider.load();
210
+ }
211
+ catch (error) {
212
+ loading = Promise.reject(error instanceof Error ? error : new Error(String(error)));
213
+ }
214
+ loading.then(() => {
215
+ this.#onProviderReady();
216
+ }, () => {
217
+ this.#onProviderFailed();
218
+ });
219
+ }
220
+ #onProviderReady() {
221
+ this.#loading = false;
222
+ if (this.#destroyed)
223
+ return;
224
+ this.#loaded = true;
225
+ if (this.#revokedWhileLoading) {
226
+ // Revoked while loading: wipe whatever the provider set up before using it again.
227
+ this.#revokedWhileLoading = false;
228
+ this.#guard(() => {
229
+ this.#provider.optOut();
230
+ });
231
+ }
232
+ if (this.#consent !== 'granted')
233
+ return;
234
+ this.#status = 'ready';
235
+ this.#guard(() => {
236
+ this.#provider.optIn();
237
+ });
238
+ this.#applyIdentity();
239
+ const pending = this.#pending;
240
+ this.#pending = [];
241
+ for (const event of pending)
242
+ this.#deliver(event);
243
+ }
244
+ #onProviderFailed() {
245
+ this.#loading = false;
246
+ this.#revokedWhileLoading = false;
247
+ if (this.#destroyed)
248
+ return;
249
+ this.#pending = [];
250
+ if (this.#consent !== 'granted')
251
+ return;
252
+ this.#status = 'unavailable';
253
+ this.#report('provider_unavailable', null);
254
+ }
255
+ #onAuthState(state) {
256
+ const identity = identityFor(state);
257
+ if (identity === null)
258
+ return;
259
+ this.#identity = identity;
260
+ this.#applyIdentity();
261
+ }
262
+ #applyIdentity() {
263
+ const identity = this.#identity;
264
+ if (identity === null || this.#status !== 'ready' || this.#consent !== 'granted')
265
+ return;
266
+ this.#guard(() => {
267
+ this.#provider.identify(identity);
268
+ });
269
+ }
270
+ #emit(name, properties, extra, protectedKeys = NO_PROTECTED_KEYS) {
271
+ if (this.#destroyed || this.#config === null)
272
+ return;
273
+ const keys = new Set([...protectedKeys, ...Object.keys(extra)]);
274
+ let sanitized;
275
+ try {
276
+ sanitized = sanitizeProperties(name, properties, keys, this.#reporter);
277
+ }
278
+ catch {
279
+ // Hostile input (throwing getters, proxies): drop the event, never the game.
280
+ this.#report('invalid_property', name);
281
+ return;
282
+ }
283
+ if (this.#consent !== 'granted')
284
+ return;
285
+ if (this.#status !== 'loading' && this.#status !== 'ready')
286
+ return;
287
+ const event = {
288
+ name,
289
+ properties: { ...sanitized, ...extra, ...this.#metadata() },
290
+ timestamp: this.#now(),
291
+ };
292
+ if (this.#status === 'ready') {
293
+ this.#deliver(event);
294
+ }
295
+ else if (this.#pending.length < MAX_PENDING_EVENTS) {
296
+ this.#pending.push({ ...event, delayed: true });
297
+ }
298
+ else {
299
+ this.#report('queue_full', name);
300
+ }
301
+ }
302
+ #deliver(event) {
303
+ this.#guard(() => {
304
+ this.#provider.capture(event);
305
+ }, event.name);
306
+ }
307
+ #metadata() {
308
+ const config = this.#config;
309
+ const metadata = {
310
+ game_id: config?.gameId ?? null,
311
+ sdk_version: SDK_VERSION,
312
+ platform: 'web',
313
+ };
314
+ if (config?.gameVersion != null)
315
+ metadata.game_version = config.gameVersion;
316
+ if (this.#session !== null)
317
+ metadata.session_id = this.#session.id;
318
+ return metadata;
319
+ }
320
+ #level(event, level) {
321
+ const normalized = normalizeLevel(level);
322
+ if (normalized === null)
323
+ this.#report('invalid_level', event);
324
+ return normalized;
325
+ }
326
+ #levelDuration(level) {
327
+ const levels = this.#session?.levels;
328
+ const startedAt = levels?.get(level);
329
+ if (levels === undefined || startedAt === undefined)
330
+ return {};
331
+ levels.delete(level);
332
+ return { duration_ms: Math.max(0, Math.round(this.#now() - startedAt)) };
333
+ }
334
+ #onVisibilityChange(session) {
335
+ if (this.#session !== session || this.#visibility === null)
336
+ return;
337
+ const now = this.#now();
338
+ if (this.#visibility.isVisible()) {
339
+ session.visibleSince ??= now;
340
+ }
341
+ else if (session.visibleSince !== null) {
342
+ session.visibleMs += now - session.visibleSince;
343
+ session.visibleSince = null;
344
+ }
345
+ }
346
+ #closeSession() {
347
+ const session = this.#session;
348
+ this.#session = null;
349
+ session?.unsubscribeVisibility?.();
350
+ }
351
+ #reporter = (code, event, property = null) => {
352
+ this.#report(code, event, property);
353
+ };
354
+ #report(code, event, property = null) {
355
+ const onIssue = this.#config?.onIssue;
356
+ if (!onIssue)
357
+ return;
358
+ try {
359
+ onIssue(Object.freeze({ code, event, property }));
360
+ }
361
+ catch {
362
+ // Diagnostics must never affect the game.
363
+ }
364
+ }
365
+ #guard(action, event = null) {
366
+ try {
367
+ action();
368
+ }
369
+ catch {
370
+ this.#report('provider_error', event);
371
+ }
372
+ }
373
+ #consentKey() {
374
+ return `pixelume.analytics.consent.${this.#config?.gameId ?? 'unconfigured'}`;
375
+ }
376
+ #storage() {
377
+ try {
378
+ if (this.#browser !== undefined)
379
+ return this.#browser.persistentStorage ?? null;
380
+ return typeof globalThis.localStorage === 'undefined' ? null : globalThis.localStorage;
381
+ }
382
+ catch {
383
+ // Blocked storage (privacy settings, sandboxed frames).
384
+ return null;
385
+ }
386
+ }
387
+ #readConsent() {
388
+ try {
389
+ const raw = this.#storage()?.getItem(this.#consentKey()) ?? null;
390
+ if (raw === null)
391
+ return null;
392
+ const value = JSON.parse(raw);
393
+ if (typeof value === 'object' &&
394
+ value !== null &&
395
+ value.version === CONSENT_VERSION &&
396
+ typeof value.granted === 'boolean') {
397
+ return value.granted;
398
+ }
399
+ return null;
400
+ }
401
+ catch {
402
+ return null;
403
+ }
404
+ }
405
+ #writeConsent(granted) {
406
+ try {
407
+ this.#storage()?.setItem(this.#consentKey(), JSON.stringify({ version: CONSENT_VERSION, granted, at: this.#now() }));
408
+ }
409
+ catch {
410
+ // Not remembered: the game asks again on the next visit.
411
+ }
412
+ }
413
+ }
414
+ function createDefaultProvider(config) {
415
+ return new PostHogAnalyticsProvider({
416
+ token: config.provider.token,
417
+ host: config.provider.host,
418
+ gameId: config.gameId,
419
+ });
420
+ }
421
+ /** `null` while authentication is still settling: keep the previous identity. */
422
+ function identityFor(state) {
423
+ if (state.status === 'authenticated')
424
+ return { player: state.player.id, separate: false };
425
+ if (state.status === 'anonymous') {
426
+ return { player: null, separate: SEPARATING_REASONS.has(state.reason) };
427
+ }
428
+ return null;
429
+ }
430
+ function randomId(browser) {
431
+ const globalCrypto = Reflect.get(globalThis, 'crypto');
432
+ const crypto = browser?.crypto ?? globalCrypto;
433
+ if (typeof crypto?.randomUUID === 'function')
434
+ return crypto.randomUUID();
435
+ const bytes = new Uint8Array(16);
436
+ if (typeof crypto?.getRandomValues === 'function') {
437
+ crypto.getRandomValues(bytes);
438
+ }
439
+ else {
440
+ for (let index = 0; index < bytes.length; index += 1) {
441
+ bytes[index] = Math.floor(Math.random() * 256);
442
+ }
443
+ }
444
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('');
445
+ }
446
+ /** `document.visibilityState`, looked up lazily; `null` outside a browser. */
447
+ function browserVisibilitySource() {
448
+ const document = Reflect.get(globalThis, 'document');
449
+ if (document === undefined || typeof document.addEventListener !== 'function')
450
+ return null;
451
+ return {
452
+ isVisible: () => document.visibilityState !== 'hidden',
453
+ subscribe: (onChange) => {
454
+ document.addEventListener('visibilitychange', onChange);
455
+ return () => {
456
+ document.removeEventListener('visibilitychange', onChange);
457
+ };
458
+ },
459
+ };
460
+ }
461
+ //# sourceMappingURL=AnalyticsModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnalyticsModule.js","sourceRoot":"","sources":["../../../src/modules/analytics/AnalyticsModule.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ResolvedAnalyticsConfig } from '../../core/config.js';\nimport type {\n PixelumeAnalyticsIssue,\n PixelumeBrowserAdapter,\n PixelumeSessionStorage,\n} from '../../core/types.js';\nimport { SDK_VERSION } from '../../version.js';\nimport type { PixelumeAuthListener, PixelumeAuthState } from '../auth/AuthModule.js';\nimport {\n NoopAnalyticsProvider,\n type AnalyticsEvent,\n type AnalyticsIdentity,\n type AnalyticsProvider,\n} from './AnalyticsProvider.js';\nimport {\n ANALYTICS_EVENTS,\n isCatalogEvent,\n isValidEventName,\n normalizeLevel,\n normalizeReason,\n sanitizeProperties,\n type AnalyticsProperties,\n type IssueReporter,\n} from './events.js';\nimport { PostHogAnalyticsProvider } from './PostHogAnalyticsProvider.js';\n\n/** Flat values accepted as event properties. Other values are dropped (see `onIssue`). */\nexport type PixelumeAnalyticsProperties = Readonly<\n Record<string, string | number | boolean | null | undefined>\n>;\n\n/**\n * - `unknown`: the player has not decided (or the choice is not remembered). Nothing is sent.\n * - `granted`: events are sent.\n * - `denied`: nothing is sent and the analytics identifiers were removed from the browser.\n */\nexport type PixelumeAnalyticsConsent = 'unknown' | 'granted' | 'denied';\n\n/**\n * - `disabled`: not configured (or the client was destroyed): every call is a no-op.\n * - `waiting_consent`: configured, consent not granted. Events are discarded, not queued.\n * - `loading`: consent granted, provider loading; events wait in a small in-memory queue.\n * - `ready`: events are being delivered.\n * - `unavailable`: the provider could not load here (no browser, blocked script/network). Events\n * are discarded; a later `setConsent(true)` tries again.\n */\nexport type PixelumeAnalyticsStatus =\n 'disabled' | 'waiting_consent' | 'loading' | 'ready' | 'unavailable';\n\n/**\n * Gameplay analytics, decoupled from the provider (PostHog today). Every method is synchronous,\n * never throws and never waits for the network: a failing provider cannot stop the game.\n *\n * A **game session** is one continuous play period defined by the game (e.g. from \"Play\" until the\n * player returns to the menu, or one match); it is unrelated to the Pixelume login session.\n */\nexport interface PixelumeAnalyticsApi {\n readonly consent: PixelumeAnalyticsConsent;\n readonly status: PixelumeAnalyticsStatus;\n /** Id of the current game session (`session_id` of every event), or `null` outside one. */\n readonly sessionId: string | null;\n /** `true` when events are (or will shortly be) delivered: configured, consented, available. */\n isEnabled(): boolean;\n /**\n * Records the player's explicit decision. `false` discards pending events, stops delivery and\n * removes the analytics identifiers from the browser; `true` after `false` starts a new\n * anonymous identity. Repeating the current value does nothing.\n */\n setConsent(granted: boolean): void;\n /** Custom event: snake_case name (1–64 chars) not in the built-in catalog. */\n track(event: string, properties?: PixelumeAnalyticsProperties): void;\n /** Starts a game session (`game_session_started`). Does nothing while one is active. */\n startSession(properties?: PixelumeAnalyticsProperties): void;\n /**\n * Ends the current game session (`game_session_ended` with `duration_ms` and\n * `visible_duration_ms`). Does nothing without an active session. Closing the tab does **not**\n * end the session: only the game knows when play really ended.\n */\n endSession(properties?: PixelumeAnalyticsProperties): void;\n levelStarted(level: string | number, properties?: PixelumeAnalyticsProperties): void;\n /** `level_completed`; carries `duration_ms` when the matching `levelStarted` was seen. */\n levelCompleted(level: string | number, properties?: PixelumeAnalyticsProperties): void;\n /** `level_failed` with an optional short `reason` (e.g. `'out_of_moves'`). */\n levelFailed(\n level: string | number,\n reason?: string,\n properties?: PixelumeAnalyticsProperties,\n ): void;\n}\n\n/** Subset of `pixelume.auth` used to follow the player's identity. */\nexport interface AnalyticsAuthSource {\n readonly state: PixelumeAuthState;\n subscribe(listener: PixelumeAuthListener): () => void;\n}\n\n/** Visibility signals; tests inject a double. */\nexport interface VisibilitySource {\n isVisible(): boolean;\n subscribe(onChange: () => void): () => void;\n}\n\nexport interface AnalyticsModuleOptions {\n readonly config: ResolvedAnalyticsConfig | null;\n readonly auth: AnalyticsAuthSource;\n readonly browser?: PixelumeBrowserAdapter | undefined;\n /** Test seams. */\n readonly createProvider?: (config: ResolvedAnalyticsConfig) => AnalyticsProvider;\n readonly visibility?: VisibilitySource | null;\n readonly now?: () => number;\n}\n\n/** Events kept while the provider loads after consent; older ones are kept, newer dropped. */\nexport const MAX_PENDING_EVENTS = 100;\nconst MAX_TRACKED_LEVELS = 50;\nconst CONSENT_VERSION = 1;\n/** Anonymous reasons meaning \"the account is no longer the one playing\". */\nconst SEPARATING_REASONS: ReadonlySet<string> = new Set(['logged_out', 'expired', 'session_ended']);\n/** Keys set by the SDK for an event, protected even when the SDK does not set them this time. */\nconst NO_PROTECTED_KEYS: ReadonlySet<string> = new Set();\nconst SESSION_END_KEYS: ReadonlySet<string> = new Set(['duration_ms', 'visible_duration_ms']);\nconst LEVEL_KEYS: ReadonlySet<string> = new Set(['level', 'duration_ms']);\nconst LEVEL_FAILED_KEYS: ReadonlySet<string> = new Set(['level', 'duration_ms', 'reason']);\n\ninterface GameSession {\n readonly id: string;\n readonly startedAt: number;\n visibleMs: number;\n visibleSince: number | null;\n readonly unsubscribeVisibility: (() => void) | null;\n readonly levels: Map<string, number>;\n}\n\nexport class AnalyticsModule implements PixelumeAnalyticsApi {\n readonly #config: ResolvedAnalyticsConfig | null;\n readonly #provider: AnalyticsProvider;\n readonly #browser: PixelumeBrowserAdapter | undefined;\n readonly #visibility: VisibilitySource | null;\n readonly #now: () => number;\n readonly #unsubscribeAuth: () => void;\n #consent: PixelumeAnalyticsConsent = 'unknown';\n #status: PixelumeAnalyticsStatus;\n #pending: AnalyticsEvent[] = [];\n #identity: AnalyticsIdentity | null = null;\n #session: GameSession | null = null;\n #loading = false;\n #loaded = false;\n /** Consent was revoked while the provider was loading: wipe it as soon as it is ready. */\n #revokedWhileLoading = false;\n #destroyed = false;\n\n constructor(options: AnalyticsModuleOptions) {\n this.#config = options.config;\n this.#browser = options.browser;\n this.#visibility =\n options.visibility === undefined ? browserVisibilitySource() : options.visibility;\n this.#now = options.now ?? (() => Date.now());\n const config = this.#config;\n if (config === null) {\n this.#provider = new NoopAnalyticsProvider();\n this.#status = 'disabled';\n this.#unsubscribeAuth = () => undefined;\n return;\n }\n this.#provider = (options.createProvider ?? createDefaultProvider)(config);\n this.#status = 'waiting_consent';\n this.#identity = identityFor(options.auth.state);\n this.#unsubscribeAuth = options.auth.subscribe((state) => {\n this.#onAuthState(state);\n });\n const remembered = config.rememberConsent ? this.#readConsent() : null;\n if (remembered === false) this.#consent = 'denied';\n if (remembered === true) {\n this.#consent = 'granted';\n // Asynchronous: constructing the client never waits for, nor fails because of, the provider.\n this.#activate();\n }\n }\n\n get consent(): PixelumeAnalyticsConsent {\n return this.#consent;\n }\n\n get status(): PixelumeAnalyticsStatus {\n return this.#status;\n }\n\n get sessionId(): string | null {\n return this.#session?.id ?? null;\n }\n\n isEnabled(): boolean {\n return this.#status === 'loading' || this.#status === 'ready';\n }\n\n setConsent(granted: boolean): void {\n const value: unknown = granted;\n if (this.#destroyed || this.#config === null || typeof value !== 'boolean') return;\n const next: PixelumeAnalyticsConsent = value ? 'granted' : 'denied';\n // Repeating the decision is a no-op, except to retry a provider that could not load.\n if (next === this.#consent && this.#status !== 'unavailable') return;\n this.#consent = next;\n if (this.#config.rememberConsent) this.#writeConsent(value);\n if (value) {\n this.#activate();\n return;\n }\n this.#pending = [];\n this.#status = 'waiting_consent';\n if (this.#loading) this.#revokedWhileLoading = true;\n this.#guard(() => {\n this.#provider.optOut();\n });\n }\n\n track(event: string, properties?: PixelumeAnalyticsProperties): void {\n if (!isValidEventName(event)) {\n this.#report('invalid_event', null);\n return;\n }\n if (isCatalogEvent(event)) {\n this.#report('reserved_event', event);\n return;\n }\n this.#emit(event, properties, {});\n }\n\n startSession(properties?: PixelumeAnalyticsProperties): void {\n if (this.#destroyed || this.#session !== null) return;\n const now = this.#now();\n // A no-op client never touches the page, not even to listen.\n const visibility = this.#config === null ? null : this.#visibility;\n const session: GameSession = {\n id: randomId(this.#browser),\n startedAt: now,\n visibleMs: 0,\n visibleSince: visibility === null || visibility.isVisible() ? now : null,\n unsubscribeVisibility:\n visibility?.subscribe(() => {\n this.#onVisibilityChange(session);\n }) ?? null,\n levels: new Map(),\n };\n this.#session = session;\n this.#emit(ANALYTICS_EVENTS.sessionStarted, properties, {});\n }\n\n endSession(properties?: PixelumeAnalyticsProperties): void {\n const session = this.#session;\n if (session === null) return;\n const now = this.#now();\n const visibleMs =\n session.visibleMs + (session.visibleSince === null ? 0 : now - session.visibleSince);\n this.#emit(\n ANALYTICS_EVENTS.sessionEnded,\n properties,\n {\n duration_ms: Math.max(0, Math.round(now - session.startedAt)),\n visible_duration_ms: Math.max(0, Math.round(visibleMs)),\n },\n SESSION_END_KEYS,\n );\n this.#closeSession();\n }\n\n levelStarted(level: string | number, properties?: PixelumeAnalyticsProperties): void {\n const normalized = this.#level(ANALYTICS_EVENTS.levelStarted, level);\n if (normalized === null) return;\n const levels = this.#session?.levels;\n if (levels !== undefined) {\n levels.delete(normalized);\n if (levels.size >= MAX_TRACKED_LEVELS) {\n const oldest = levels.keys().next();\n if (oldest.done !== true) levels.delete(oldest.value);\n }\n levels.set(normalized, this.#now());\n }\n this.#emit(ANALYTICS_EVENTS.levelStarted, properties, { level: normalized }, LEVEL_KEYS);\n }\n\n levelCompleted(level: string | number, properties?: PixelumeAnalyticsProperties): void {\n const normalized = this.#level(ANALYTICS_EVENTS.levelCompleted, level);\n if (normalized === null) return;\n this.#emit(\n ANALYTICS_EVENTS.levelCompleted,\n properties,\n { level: normalized, ...this.#levelDuration(normalized) },\n LEVEL_KEYS,\n );\n }\n\n levelFailed(\n level: string | number,\n reason?: string,\n properties?: PixelumeAnalyticsProperties,\n ): void {\n const normalized = this.#level(ANALYTICS_EVENTS.levelFailed, level);\n if (normalized === null) return;\n const extra: AnalyticsProperties = { level: normalized, ...this.#levelDuration(normalized) };\n if (reason !== undefined) {\n const value = normalizeReason(reason);\n if (value === null) this.#report('invalid_property', ANALYTICS_EVENTS.levelFailed, 'reason');\n else extra.reason = value;\n }\n this.#emit(ANALYTICS_EVENTS.levelFailed, properties, extra, LEVEL_FAILED_KEYS);\n }\n\n /**\n * @internal Called by `PixelumeClient.destroy()`: releases listeners and stops delivery. Does not\n * end the game session nor change consent; posthog may still flush what it already accepted.\n */\n dispose(): void {\n if (this.#destroyed) return;\n this.#destroyed = true;\n this.#unsubscribeAuth();\n this.#closeSession();\n this.#pending = [];\n this.#status = 'disabled';\n this.#guard(() => {\n this.#provider.detach();\n });\n }\n\n #activate(): void {\n if (this.#destroyed || this.#consent !== 'granted') return;\n if (this.#loading) {\n this.#status = 'loading';\n return;\n }\n if (this.#loaded) {\n this.#status = 'ready';\n this.#guard(() => {\n this.#provider.optIn();\n });\n this.#applyIdentity();\n return;\n }\n this.#status = 'loading';\n this.#loading = true;\n let loading: Promise<void>;\n try {\n loading = this.#provider.load();\n } catch (error) {\n loading = Promise.reject(error instanceof Error ? error : new Error(String(error)));\n }\n loading.then(\n () => {\n this.#onProviderReady();\n },\n () => {\n this.#onProviderFailed();\n },\n );\n }\n\n #onProviderReady(): void {\n this.#loading = false;\n if (this.#destroyed) return;\n this.#loaded = true;\n if (this.#revokedWhileLoading) {\n // Revoked while loading: wipe whatever the provider set up before using it again.\n this.#revokedWhileLoading = false;\n this.#guard(() => {\n this.#provider.optOut();\n });\n }\n if (this.#consent !== 'granted') return;\n this.#status = 'ready';\n this.#guard(() => {\n this.#provider.optIn();\n });\n this.#applyIdentity();\n const pending = this.#pending;\n this.#pending = [];\n for (const event of pending) this.#deliver(event);\n }\n\n #onProviderFailed(): void {\n this.#loading = false;\n this.#revokedWhileLoading = false;\n if (this.#destroyed) return;\n this.#pending = [];\n if (this.#consent !== 'granted') return;\n this.#status = 'unavailable';\n this.#report('provider_unavailable', null);\n }\n\n #onAuthState(state: PixelumeAuthState): void {\n const identity = identityFor(state);\n if (identity === null) return;\n this.#identity = identity;\n this.#applyIdentity();\n }\n\n #applyIdentity(): void {\n const identity = this.#identity;\n if (identity === null || this.#status !== 'ready' || this.#consent !== 'granted') return;\n this.#guard(() => {\n this.#provider.identify(identity);\n });\n }\n\n #emit(\n name: string,\n properties: PixelumeAnalyticsProperties | undefined,\n extra: AnalyticsProperties,\n protectedKeys: ReadonlySet<string> = NO_PROTECTED_KEYS,\n ): void {\n if (this.#destroyed || this.#config === null) return;\n const keys = new Set([...protectedKeys, ...Object.keys(extra)]);\n let sanitized: AnalyticsProperties;\n try {\n sanitized = sanitizeProperties(name, properties, keys, this.#reporter);\n } catch {\n // Hostile input (throwing getters, proxies): drop the event, never the game.\n this.#report('invalid_property', name);\n return;\n }\n if (this.#consent !== 'granted') return;\n if (this.#status !== 'loading' && this.#status !== 'ready') return;\n const event: AnalyticsEvent = {\n name,\n properties: { ...sanitized, ...extra, ...this.#metadata() },\n timestamp: this.#now(),\n };\n if (this.#status === 'ready') {\n this.#deliver(event);\n } else if (this.#pending.length < MAX_PENDING_EVENTS) {\n this.#pending.push({ ...event, delayed: true });\n } else {\n this.#report('queue_full', name);\n }\n }\n\n #deliver(event: AnalyticsEvent): void {\n this.#guard(() => {\n this.#provider.capture(event);\n }, event.name);\n }\n\n #metadata(): AnalyticsProperties {\n const config = this.#config;\n const metadata: AnalyticsProperties = {\n game_id: config?.gameId ?? null,\n sdk_version: SDK_VERSION,\n platform: 'web',\n };\n if (config?.gameVersion != null) metadata.game_version = config.gameVersion;\n if (this.#session !== null) metadata.session_id = this.#session.id;\n return metadata;\n }\n\n #level(event: string, level: unknown): string | null {\n const normalized = normalizeLevel(level);\n if (normalized === null) this.#report('invalid_level', event);\n return normalized;\n }\n\n #levelDuration(level: string): { duration_ms?: number } {\n const levels = this.#session?.levels;\n const startedAt = levels?.get(level);\n if (levels === undefined || startedAt === undefined) return {};\n levels.delete(level);\n return { duration_ms: Math.max(0, Math.round(this.#now() - startedAt)) };\n }\n\n #onVisibilityChange(session: GameSession): void {\n if (this.#session !== session || this.#visibility === null) return;\n const now = this.#now();\n if (this.#visibility.isVisible()) {\n session.visibleSince ??= now;\n } else if (session.visibleSince !== null) {\n session.visibleMs += now - session.visibleSince;\n session.visibleSince = null;\n }\n }\n\n #closeSession(): void {\n const session = this.#session;\n this.#session = null;\n session?.unsubscribeVisibility?.();\n }\n\n readonly #reporter: IssueReporter = (code, event, property = null) => {\n this.#report(code, event, property);\n };\n\n #report(\n code: PixelumeAnalyticsIssue['code'],\n event: string | null,\n property: string | null = null,\n ): void {\n const onIssue = this.#config?.onIssue;\n if (!onIssue) return;\n try {\n onIssue(Object.freeze({ code, event, property }));\n } catch {\n // Diagnostics must never affect the game.\n }\n }\n\n #guard(action: () => void, event: string | null = null): void {\n try {\n action();\n } catch {\n this.#report('provider_error', event);\n }\n }\n\n #consentKey(): string {\n return `pixelume.analytics.consent.${this.#config?.gameId ?? 'unconfigured'}`;\n }\n\n #storage(): PixelumeSessionStorage | null {\n try {\n if (this.#browser !== undefined) return this.#browser.persistentStorage ?? null;\n return typeof globalThis.localStorage === 'undefined' ? null : globalThis.localStorage;\n } catch {\n // Blocked storage (privacy settings, sandboxed frames).\n return null;\n }\n }\n\n #readConsent(): boolean | null {\n try {\n const raw = this.#storage()?.getItem(this.#consentKey()) ?? null;\n if (raw === null) return null;\n const value: unknown = JSON.parse(raw);\n if (\n typeof value === 'object' &&\n value !== null &&\n (value as { version?: unknown }).version === CONSENT_VERSION &&\n typeof (value as { granted?: unknown }).granted === 'boolean'\n ) {\n return (value as { granted: boolean }).granted;\n }\n return null;\n } catch {\n return null;\n }\n }\n\n #writeConsent(granted: boolean): void {\n try {\n this.#storage()?.setItem(\n this.#consentKey(),\n JSON.stringify({ version: CONSENT_VERSION, granted, at: this.#now() }),\n );\n } catch {\n // Not remembered: the game asks again on the next visit.\n }\n }\n}\n\nfunction createDefaultProvider(config: ResolvedAnalyticsConfig): AnalyticsProvider {\n return new PostHogAnalyticsProvider({\n token: config.provider.token,\n host: config.provider.host,\n gameId: config.gameId,\n });\n}\n\n/** `null` while authentication is still settling: keep the previous identity. */\nfunction identityFor(state: PixelumeAuthState): AnalyticsIdentity | null {\n if (state.status === 'authenticated') return { player: state.player.id, separate: false };\n if (state.status === 'anonymous') {\n return { player: null, separate: SEPARATING_REASONS.has(state.reason) };\n }\n return null;\n}\n\nfunction randomId(browser: PixelumeBrowserAdapter | undefined): string {\n const globalCrypto = Reflect.get(globalThis, 'crypto') as Crypto | undefined;\n const crypto = browser?.crypto ?? globalCrypto;\n if (typeof crypto?.randomUUID === 'function') return crypto.randomUUID();\n const bytes = new Uint8Array(16);\n if (typeof crypto?.getRandomValues === 'function') {\n crypto.getRandomValues(bytes);\n } else {\n for (let index = 0; index < bytes.length; index += 1) {\n bytes[index] = Math.floor(Math.random() * 256);\n }\n }\n return [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\n/** `document.visibilityState`, looked up lazily; `null` outside a browser. */\nfunction browserVisibilitySource(): VisibilitySource | null {\n const document = Reflect.get(globalThis, 'document') as Document | undefined;\n if (document === undefined || typeof document.addEventListener !== 'function') return null;\n return {\n isVisible: () => document.visibilityState !== 'hidden',\n subscribe: (onChange) => {\n document.addEventListener('visibilitychange', onChange);\n return () => {\n document.removeEventListener('visibilitychange', onChange);\n };\n },\n };\n}\n"]}
@@ -0,0 +1,51 @@
1
+ import type { AnalyticsProperties } from './events.js';
2
+ /** One validated, authorized event, ready for a transport. */
3
+ export interface AnalyticsEvent {
4
+ readonly name: string;
5
+ readonly properties: AnalyticsProperties;
6
+ /** Local time (ms since epoch) when the game reported it. */
7
+ readonly timestamp: number;
8
+ /** Waited in the module's queue while the provider loaded: deliver with `timestamp`. */
9
+ readonly delayed?: true;
10
+ }
11
+ /**
12
+ * Identity the provider should use:
13
+ * - `player`: a signed-in Pixelume account (internal, non-sensitive id);
14
+ * - `null` with `separate: true`: the account is gone (logout, expiry): start a new anonymous id;
15
+ * - `null` with `separate: false`: a visitor; keep whatever anonymous id exists.
16
+ */
17
+ export interface AnalyticsIdentity {
18
+ readonly player: string | null;
19
+ readonly separate: boolean;
20
+ }
21
+ /**
22
+ * Transport seam behind `pixelume.analytics`. The module validates events, applies consent and
23
+ * owns game sessions; a provider only delivers. Providers must never throw synchronously into the
24
+ * game: the module still guards every call. Internal: not part of the public API.
25
+ */
26
+ export interface AnalyticsProvider {
27
+ readonly name: 'posthog' | 'none';
28
+ /** Prepares the transport. Called only after consent. Rejects when it cannot run here. */
29
+ load(): Promise<void>;
30
+ /** Consent granted (again): allow capture and persistence. */
31
+ optIn(): void;
32
+ /**
33
+ * Consent revoked: stop capturing, discard anything not yet sent and forget the analytics
34
+ * identifiers stored in the browser.
35
+ */
36
+ optOut(): void;
37
+ capture(event: AnalyticsEvent): void;
38
+ identify(identity: AnalyticsIdentity): void;
39
+ /** The owning client was destroyed: stop forwarding. Shared resources may stay for reuse. */
40
+ detach(): void;
41
+ }
42
+ /** Used when analytics is disabled or not configured: does nothing, loads nothing. */
43
+ export declare class NoopAnalyticsProvider implements AnalyticsProvider {
44
+ readonly name = "none";
45
+ load(): Promise<void>;
46
+ optIn(): void;
47
+ optOut(): void;
48
+ capture(): void;
49
+ identify(): void;
50
+ detach(): void;
51
+ }
@@ -0,0 +1,23 @@
1
+ /** Used when analytics is disabled or not configured: does nothing, loads nothing. */
2
+ export class NoopAnalyticsProvider {
3
+ name = 'none';
4
+ load() {
5
+ return Promise.resolve();
6
+ }
7
+ optIn() {
8
+ // Nothing to enable.
9
+ }
10
+ optOut() {
11
+ // Nothing stored.
12
+ }
13
+ capture() {
14
+ // Dropped by design.
15
+ }
16
+ identify() {
17
+ // No identity to keep.
18
+ }
19
+ detach() {
20
+ // Nothing to release.
21
+ }
22
+ }
23
+ //# sourceMappingURL=AnalyticsProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnalyticsProvider.js","sourceRoot":"","sources":["../../../src/modules/analytics/AnalyticsProvider.ts"],"names":[],"mappings":"AA6CA,sFAAsF;AACtF,MAAM,OAAO,qBAAqB;IACvB,IAAI,GAAG,MAAM,CAAC;IAEvB,IAAI;QACF,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,KAAK;QACH,qBAAqB;IACvB,CAAC;IAED,MAAM;QACJ,kBAAkB;IACpB,CAAC;IAED,OAAO;QACL,qBAAqB;IACvB,CAAC;IAED,QAAQ;QACN,uBAAuB;IACzB,CAAC;IAED,MAAM;QACJ,sBAAsB;IACxB,CAAC;CACF","sourcesContent":["import type { AnalyticsProperties } from './events.js';\n\n/** One validated, authorized event, ready for a transport. */\nexport interface AnalyticsEvent {\n readonly name: string;\n readonly properties: AnalyticsProperties;\n /** Local time (ms since epoch) when the game reported it. */\n readonly timestamp: number;\n /** Waited in the module's queue while the provider loaded: deliver with `timestamp`. */\n readonly delayed?: true;\n}\n\n/**\n * Identity the provider should use:\n * - `player`: a signed-in Pixelume account (internal, non-sensitive id);\n * - `null` with `separate: true`: the account is gone (logout, expiry): start a new anonymous id;\n * - `null` with `separate: false`: a visitor; keep whatever anonymous id exists.\n */\nexport interface AnalyticsIdentity {\n readonly player: string | null;\n readonly separate: boolean;\n}\n\n/**\n * Transport seam behind `pixelume.analytics`. The module validates events, applies consent and\n * owns game sessions; a provider only delivers. Providers must never throw synchronously into the\n * game: the module still guards every call. Internal: not part of the public API.\n */\nexport interface AnalyticsProvider {\n readonly name: 'posthog' | 'none';\n /** Prepares the transport. Called only after consent. Rejects when it cannot run here. */\n load(): Promise<void>;\n /** Consent granted (again): allow capture and persistence. */\n optIn(): void;\n /**\n * Consent revoked: stop capturing, discard anything not yet sent and forget the analytics\n * identifiers stored in the browser.\n */\n optOut(): void;\n capture(event: AnalyticsEvent): void;\n identify(identity: AnalyticsIdentity): void;\n /** The owning client was destroyed: stop forwarding. Shared resources may stay for reuse. */\n detach(): void;\n}\n\n/** Used when analytics is disabled or not configured: does nothing, loads nothing. */\nexport class NoopAnalyticsProvider implements AnalyticsProvider {\n readonly name = 'none';\n\n load(): Promise<void> {\n return Promise.resolve();\n }\n\n optIn(): void {\n // Nothing to enable.\n }\n\n optOut(): void {\n // Nothing stored.\n }\n\n capture(): void {\n // Dropped by design.\n }\n\n identify(): void {\n // No identity to keep.\n }\n\n detach(): void {\n // Nothing to release.\n }\n}\n"]}
@@ -0,0 +1,39 @@
1
+ import type { PostHogConfig } from 'posthog-js/dist/module.slim.no-external.js';
2
+ import type { AnalyticsEvent, AnalyticsIdentity, AnalyticsProvider } from './AnalyticsProvider.js';
3
+ /** The part of the `posthog-js` instance API the provider relies on. */
4
+ export interface PostHogInstanceLike {
5
+ init(token: string, config: Partial<PostHogConfig>): unknown;
6
+ capture(event: string, properties?: Record<string, unknown> | null, options?: {
7
+ readonly timestamp?: Date;
8
+ }): unknown;
9
+ identify(distinctId: string): void;
10
+ reset(resetDeviceId?: boolean): void;
11
+ opt_in_capturing(options?: {
12
+ readonly captureEventName?: false | null;
13
+ }): void;
14
+ opt_out_capturing(): void;
15
+ has_opted_out_capturing(): boolean;
16
+ get_distinct_id(): string;
17
+ get_property(key: string): unknown;
18
+ }
19
+ export interface PostHogModuleLike {
20
+ readonly PostHog: new () => PostHogInstanceLike;
21
+ }
22
+ export interface PostHogProviderOptions {
23
+ readonly token: string;
24
+ readonly host: string;
25
+ readonly gameId: string;
26
+ /** Test seam; defaults to a dynamic import of the slim, self-contained posthog-js bundle. */
27
+ readonly loadModule?: () => Promise<PostHogModuleLike>;
28
+ }
29
+ export declare class PostHogAnalyticsProvider implements AnalyticsProvider {
30
+ #private;
31
+ readonly name = "posthog";
32
+ constructor(options: PostHogProviderOptions);
33
+ load(): Promise<void>;
34
+ optIn(): void;
35
+ optOut(): void;
36
+ capture(event: AnalyticsEvent): void;
37
+ identify(identity: AnalyticsIdentity): void;
38
+ detach(): void;
39
+ }