@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,255 @@
1
+ /**
2
+ * Slim build without extensions (no autocapture, replay, surveys, toolbar...) and without
3
+ * `loadExternalDependency`: it never injects `<script>` tags. The dynamic import keeps posthog-js
4
+ * out of the game's main bundle and out of Node/SSR: it only runs after consent, in a browser.
5
+ */
6
+ // Typed against the real posthog-js declarations: an upgrade that changes a method we rely on
7
+ // fails the type check here.
8
+ const defaultLoadModule = () => import('posthog-js/dist/module.slim.no-external.js');
9
+ /** Provider events that may leave the browser; every other `$event` is dropped. */
10
+ const ALLOWED_PROVIDER_EVENTS = new Set(['$identify', '$set']);
11
+ /** Properties removed from every outgoing event. */
12
+ const DROPPED_PROVIDER_PROPERTIES = ['$raw_user_agent'];
13
+ /**
14
+ * posthog-js has no teardown: every initialized instance keeps its page listeners for the life of
15
+ * the page. Instances are therefore created once per (host, token, game) and reused by later
16
+ * clients (SPA remounts, hot reload) instead of being initialized again.
17
+ */
18
+ const sharedInstances = new Map();
19
+ export class PostHogAnalyticsProvider {
20
+ name = 'posthog';
21
+ #options;
22
+ #shared = null;
23
+ #instance = null;
24
+ #attached = true;
25
+ constructor(options) {
26
+ this.#options = options;
27
+ }
28
+ async load() {
29
+ if (this.#instance !== null)
30
+ return;
31
+ if (!hasBrowserGlobals())
32
+ throw new Error('PostHog analytics needs a browser environment');
33
+ const shared = this.#sharedInstance();
34
+ this.#shared = shared;
35
+ const instance = await shared.ready;
36
+ if (this.#attached)
37
+ this.#instance = instance;
38
+ }
39
+ optIn() {
40
+ const instance = this.#instance;
41
+ if (instance === null || this.#shared === null)
42
+ return;
43
+ this.#shared.gate.open = true;
44
+ // Undo a previous revocation; never capture posthog's own `$opt_in` event.
45
+ if (instance.has_opted_out_capturing())
46
+ instance.opt_in_capturing({ captureEventName: false });
47
+ }
48
+ optOut() {
49
+ // Also reached before `load()`: another client of the same game may have opened the instance.
50
+ const shared = this.#shared ?? sharedInstances.get(this.#key()) ?? null;
51
+ if (shared === null)
52
+ return;
53
+ shared.gate.open = false;
54
+ const instance = shared.instance;
55
+ if (instance === null)
56
+ return;
57
+ discardPendingRequests(instance);
58
+ // `reset()` clears posthog's consent state, so it must come first; then opting out disables
59
+ // persistence and removes what posthog stored (`opt_out_persistence_by_default`).
60
+ instance.reset(true);
61
+ instance.opt_out_capturing();
62
+ // Disabling persistence leaves the tab's window id in sessionStorage; remove it as well.
63
+ removeSessionKeys(`ph_pixelume_${this.#options.gameId}`);
64
+ }
65
+ capture(event) {
66
+ const instance = this.#instance;
67
+ if (instance === null || !this.#attached || this.#shared?.gate.open !== true)
68
+ return;
69
+ // Any options object makes posthog-js skip its batch and send at once, so live events go
70
+ // without one (posthog stamps them); only events that waited for the load keep their time.
71
+ if (event.delayed === true) {
72
+ instance.capture(event.name, { ...event.properties }, { timestamp: new Date(event.timestamp) });
73
+ }
74
+ else {
75
+ instance.capture(event.name, { ...event.properties });
76
+ }
77
+ }
78
+ identify(identity) {
79
+ const instance = this.#instance;
80
+ if (instance === null || !this.#attached || this.#shared?.gate.open !== true)
81
+ return;
82
+ const identified = instance.get_property('$user_state') === 'identified';
83
+ if (identity.player === null) {
84
+ if (identity.separate && identified)
85
+ instance.reset();
86
+ return;
87
+ }
88
+ if (identified && instance.get_distinct_id() === identity.player)
89
+ return;
90
+ // Another account was identified on this browser: never merge two players.
91
+ if (identified)
92
+ instance.reset();
93
+ // From an anonymous id, `identify` links the visitor's earlier events to the player.
94
+ instance.identify(identity.player);
95
+ }
96
+ detach() {
97
+ this.#attached = false;
98
+ this.#instance = null;
99
+ }
100
+ #key() {
101
+ const { token, host, gameId } = this.#options;
102
+ return `${host}\n${token}\n${gameId}`;
103
+ }
104
+ #sharedInstance() {
105
+ const { token, host, gameId } = this.#options;
106
+ const key = this.#key();
107
+ const existing = sharedInstances.get(key);
108
+ if (existing !== undefined)
109
+ return existing;
110
+ const gate = { open: true };
111
+ const loadModule = this.#options.loadModule ?? defaultLoadModule;
112
+ const ready = loadModule().then((module) => {
113
+ const instance = new module.PostHog();
114
+ instance.init(token, posthogConfig(host, gameId, gate));
115
+ shared.instance = instance;
116
+ return instance;
117
+ });
118
+ const shared = { ready, instance: null, gate };
119
+ sharedInstances.set(key, shared);
120
+ // A failed chunk download (offline, CSP) must not poison later attempts.
121
+ ready.catch(() => {
122
+ if (sharedInstances.get(key) === shared)
123
+ sharedInstances.delete(key);
124
+ });
125
+ return shared;
126
+ }
127
+ }
128
+ /**
129
+ * Everything implicit is off: only events sent through `pixelume.analytics` (and `$identify`)
130
+ * reach PostHog. No remote config or flags request, no external scripts, no cookies.
131
+ */
132
+ function posthogConfig(host, gameId, gate) {
133
+ return {
134
+ api_host: host,
135
+ // Separate storage from any other PostHog installation on the same site.
136
+ persistence: 'localStorage',
137
+ persistence_name: `pixelume_${gameId}`,
138
+ consent_persistence_name: `__ph_opt_in_out_pixelume_${gameId}`,
139
+ opt_out_capturing_persistence_type: 'localStorage',
140
+ opt_out_persistence_by_default: true,
141
+ cross_subdomain_cookie: false,
142
+ person_profiles: 'identified_only',
143
+ autocapture: false,
144
+ rageclick: false,
145
+ capture_pageview: false,
146
+ capture_pageleave: false,
147
+ capture_exceptions: false,
148
+ capture_performance: false,
149
+ capture_heatmaps: false,
150
+ enable_heatmaps: false,
151
+ capture_dead_clicks: false,
152
+ disable_scroll_properties: true,
153
+ disable_session_recording: true,
154
+ disable_surveys: true,
155
+ disable_surveys_automatic_display: true,
156
+ disable_product_tours: true,
157
+ disable_conversations: true,
158
+ disable_web_experiments: true,
159
+ disable_external_dependency_loading: true,
160
+ advanced_disable_flags: true,
161
+ advanced_disable_decide: true,
162
+ advanced_disable_feature_flags: true,
163
+ advanced_disable_feature_flags_on_first_load: true,
164
+ remote_config_refresh_interval_ms: 0,
165
+ save_referrer: false,
166
+ save_campaign_params: false,
167
+ mask_personal_data_properties: true,
168
+ debug: false,
169
+ before_send: (event) => {
170
+ if (event === null || !gate.open)
171
+ return null;
172
+ if (event.event.startsWith('$') && !ALLOWED_PROVIDER_EVENTS.has(event.event))
173
+ return null;
174
+ try {
175
+ const properties = event.properties;
176
+ scrub(properties);
177
+ scrub(event.$set);
178
+ scrub(event.$set_once);
179
+ if (typeof properties === 'object' && properties !== null) {
180
+ scrub(properties.$set);
181
+ scrub(properties.$set_once);
182
+ }
183
+ return event;
184
+ }
185
+ catch {
186
+ // Fail closed: an event that cannot be cleaned is not sent.
187
+ return null;
188
+ }
189
+ },
190
+ };
191
+ }
192
+ /** Strips query strings and fragments (which may carry codes or personal data) from URLs. */
193
+ function scrub(value) {
194
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
195
+ return;
196
+ const properties = value;
197
+ for (const key of DROPPED_PROVIDER_PROPERTIES)
198
+ Reflect.deleteProperty(properties, key);
199
+ for (const [key, value] of Object.entries(properties)) {
200
+ if (!key.startsWith('$') || typeof value !== 'string' || !/^https?:\/\//iu.test(value)) {
201
+ continue;
202
+ }
203
+ try {
204
+ const url = new URL(value);
205
+ properties[key] = `${url.origin}${url.pathname}`;
206
+ }
207
+ catch {
208
+ Reflect.deleteProperty(properties, key);
209
+ }
210
+ }
211
+ }
212
+ /**
213
+ * `opt_out_capturing()` stops new captures but does not empty posthog's batch and retry queues,
214
+ * which would still be sent (seen with 1.435.8). Revocation must not leak them, so they are cleared
215
+ * here. The fields are internal to posthog-js: the version is pinned and a test runs this path
216
+ * against the real library.
217
+ */
218
+ function discardPendingRequests(instance) {
219
+ for (const name of ['_requestQueue', '_retryQueue']) {
220
+ try {
221
+ const queue = instance[name];
222
+ if (queue !== undefined && Array.isArray(queue._queue))
223
+ queue._queue = [];
224
+ }
225
+ catch {
226
+ // Best effort; the gate in `before_send` already blocks new events.
227
+ }
228
+ }
229
+ }
230
+ /**
231
+ * Removes `prefix` and `prefix_*`. Game ids never contain `_`, so another game whose id merely
232
+ * starts with this one (`rail` / `rail-puzzle`) keeps its keys.
233
+ */
234
+ function removeSessionKeys(prefix) {
235
+ try {
236
+ const storage = globalThis.sessionStorage;
237
+ const keys = [];
238
+ for (let index = 0; index < storage.length; index += 1) {
239
+ const key = storage.key(index);
240
+ if (key !== null && (key === prefix || key.startsWith(`${prefix}_`)))
241
+ keys.push(key);
242
+ }
243
+ for (const key of keys)
244
+ storage.removeItem(key);
245
+ }
246
+ catch {
247
+ // Blocked storage: nothing was written there either.
248
+ }
249
+ }
250
+ function hasBrowserGlobals() {
251
+ return (typeof globalThis.window === 'object' &&
252
+ typeof globalThis.document === 'object' &&
253
+ typeof globalThis.navigator === 'object');
254
+ }
255
+ //# sourceMappingURL=PostHogAnalyticsProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostHogAnalyticsProvider.js","sourceRoot":"","sources":["../../../src/modules/analytics/PostHogAnalyticsProvider.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CaptureResult, PostHogConfig } from 'posthog-js/dist/module.slim.no-external.js';\nimport type { AnalyticsEvent, AnalyticsIdentity, AnalyticsProvider } from './AnalyticsProvider.js';\n\n/** The part of the `posthog-js` instance API the provider relies on. */\nexport interface PostHogInstanceLike {\n init(token: string, config: Partial<PostHogConfig>): unknown;\n capture(\n event: string,\n properties?: Record<string, unknown> | null,\n options?: { readonly timestamp?: Date },\n ): unknown;\n identify(distinctId: string): void;\n reset(resetDeviceId?: boolean): void;\n opt_in_capturing(options?: { readonly captureEventName?: false | null }): void;\n opt_out_capturing(): void;\n has_opted_out_capturing(): boolean;\n get_distinct_id(): string;\n get_property(key: string): unknown;\n}\n\nexport interface PostHogModuleLike {\n readonly PostHog: new () => PostHogInstanceLike;\n}\n\nexport interface PostHogProviderOptions {\n readonly token: string;\n readonly host: string;\n readonly gameId: string;\n /** Test seam; defaults to a dynamic import of the slim, self-contained posthog-js bundle. */\n readonly loadModule?: () => Promise<PostHogModuleLike>;\n}\n\n/**\n * Slim build without extensions (no autocapture, replay, surveys, toolbar...) and without\n * `loadExternalDependency`: it never injects `<script>` tags. The dynamic import keeps posthog-js\n * out of the game's main bundle and out of Node/SSR: it only runs after consent, in a browser.\n */\n// Typed against the real posthog-js declarations: an upgrade that changes a method we rely on\n// fails the type check here.\nconst defaultLoadModule = (): Promise<PostHogModuleLike> =>\n import('posthog-js/dist/module.slim.no-external.js');\n\n/** Provider events that may leave the browser; every other `$event` is dropped. */\nconst ALLOWED_PROVIDER_EVENTS: ReadonlySet<string> = new Set(['$identify', '$set']);\n/** Properties removed from every outgoing event. */\nconst DROPPED_PROVIDER_PROPERTIES = ['$raw_user_agent'] as const;\n\ninterface SharedInstance {\n readonly ready: Promise<PostHogInstanceLike>;\n /** Set once `ready` resolves. */\n instance: PostHogInstanceLike | null;\n /** Shared by every provider of this instance; closed while consent is not granted. */\n readonly gate: { open: boolean };\n}\n\n/**\n * posthog-js has no teardown: every initialized instance keeps its page listeners for the life of\n * the page. Instances are therefore created once per (host, token, game) and reused by later\n * clients (SPA remounts, hot reload) instead of being initialized again.\n */\nconst sharedInstances = new Map<string, SharedInstance>();\n\nexport class PostHogAnalyticsProvider implements AnalyticsProvider {\n readonly name = 'posthog';\n readonly #options: PostHogProviderOptions;\n #shared: SharedInstance | null = null;\n #instance: PostHogInstanceLike | null = null;\n #attached = true;\n\n constructor(options: PostHogProviderOptions) {\n this.#options = options;\n }\n\n async load(): Promise<void> {\n if (this.#instance !== null) return;\n if (!hasBrowserGlobals()) throw new Error('PostHog analytics needs a browser environment');\n const shared = this.#sharedInstance();\n this.#shared = shared;\n const instance = await shared.ready;\n if (this.#attached) this.#instance = instance;\n }\n\n optIn(): void {\n const instance = this.#instance;\n if (instance === null || this.#shared === null) return;\n this.#shared.gate.open = true;\n // Undo a previous revocation; never capture posthog's own `$opt_in` event.\n if (instance.has_opted_out_capturing()) instance.opt_in_capturing({ captureEventName: false });\n }\n\n optOut(): void {\n // Also reached before `load()`: another client of the same game may have opened the instance.\n const shared = this.#shared ?? sharedInstances.get(this.#key()) ?? null;\n if (shared === null) return;\n shared.gate.open = false;\n const instance = shared.instance;\n if (instance === null) return;\n discardPendingRequests(instance);\n // `reset()` clears posthog's consent state, so it must come first; then opting out disables\n // persistence and removes what posthog stored (`opt_out_persistence_by_default`).\n instance.reset(true);\n instance.opt_out_capturing();\n // Disabling persistence leaves the tab's window id in sessionStorage; remove it as well.\n removeSessionKeys(`ph_pixelume_${this.#options.gameId}`);\n }\n\n capture(event: AnalyticsEvent): void {\n const instance = this.#instance;\n if (instance === null || !this.#attached || this.#shared?.gate.open !== true) return;\n // Any options object makes posthog-js skip its batch and send at once, so live events go\n // without one (posthog stamps them); only events that waited for the load keep their time.\n if (event.delayed === true) {\n instance.capture(\n event.name,\n { ...event.properties },\n { timestamp: new Date(event.timestamp) },\n );\n } else {\n instance.capture(event.name, { ...event.properties });\n }\n }\n\n identify(identity: AnalyticsIdentity): void {\n const instance = this.#instance;\n if (instance === null || !this.#attached || this.#shared?.gate.open !== true) return;\n const identified = instance.get_property('$user_state') === 'identified';\n if (identity.player === null) {\n if (identity.separate && identified) instance.reset();\n return;\n }\n if (identified && instance.get_distinct_id() === identity.player) return;\n // Another account was identified on this browser: never merge two players.\n if (identified) instance.reset();\n // From an anonymous id, `identify` links the visitor's earlier events to the player.\n instance.identify(identity.player);\n }\n\n detach(): void {\n this.#attached = false;\n this.#instance = null;\n }\n\n #key(): string {\n const { token, host, gameId } = this.#options;\n return `${host}\\n${token}\\n${gameId}`;\n }\n\n #sharedInstance(): SharedInstance {\n const { token, host, gameId } = this.#options;\n const key = this.#key();\n const existing = sharedInstances.get(key);\n if (existing !== undefined) return existing;\n const gate = { open: true };\n const loadModule = this.#options.loadModule ?? defaultLoadModule;\n const ready = loadModule().then((module) => {\n const instance = new module.PostHog();\n instance.init(token, posthogConfig(host, gameId, gate));\n shared.instance = instance;\n return instance;\n });\n const shared: SharedInstance = { ready, instance: null, gate };\n sharedInstances.set(key, shared);\n // A failed chunk download (offline, CSP) must not poison later attempts.\n ready.catch(() => {\n if (sharedInstances.get(key) === shared) sharedInstances.delete(key);\n });\n return shared;\n }\n}\n\n/**\n * Everything implicit is off: only events sent through `pixelume.analytics` (and `$identify`)\n * reach PostHog. No remote config or flags request, no external scripts, no cookies.\n */\nfunction posthogConfig(\n host: string,\n gameId: string,\n gate: { readonly open: boolean },\n): Partial<PostHogConfig> {\n return {\n api_host: host,\n // Separate storage from any other PostHog installation on the same site.\n persistence: 'localStorage',\n persistence_name: `pixelume_${gameId}`,\n consent_persistence_name: `__ph_opt_in_out_pixelume_${gameId}`,\n opt_out_capturing_persistence_type: 'localStorage',\n opt_out_persistence_by_default: true,\n cross_subdomain_cookie: false,\n person_profiles: 'identified_only',\n autocapture: false,\n rageclick: false,\n capture_pageview: false,\n capture_pageleave: false,\n capture_exceptions: false,\n capture_performance: false,\n capture_heatmaps: false,\n enable_heatmaps: false,\n capture_dead_clicks: false,\n disable_scroll_properties: true,\n disable_session_recording: true,\n disable_surveys: true,\n disable_surveys_automatic_display: true,\n disable_product_tours: true,\n disable_conversations: true,\n disable_web_experiments: true,\n disable_external_dependency_loading: true,\n advanced_disable_flags: true,\n advanced_disable_decide: true,\n advanced_disable_feature_flags: true,\n advanced_disable_feature_flags_on_first_load: true,\n remote_config_refresh_interval_ms: 0,\n save_referrer: false,\n save_campaign_params: false,\n mask_personal_data_properties: true,\n debug: false,\n before_send: (event: CaptureResult | null): CaptureResult | null => {\n if (event === null || !gate.open) return null;\n if (event.event.startsWith('$') && !ALLOWED_PROVIDER_EVENTS.has(event.event)) return null;\n try {\n const properties: unknown = event.properties;\n scrub(properties);\n scrub(event.$set);\n scrub(event.$set_once);\n if (typeof properties === 'object' && properties !== null) {\n scrub((properties as Record<string, unknown>).$set);\n scrub((properties as Record<string, unknown>).$set_once);\n }\n return event;\n } catch {\n // Fail closed: an event that cannot be cleaned is not sent.\n return null;\n }\n },\n };\n}\n\n/** Strips query strings and fragments (which may carry codes or personal data) from URLs. */\nfunction scrub(value: unknown): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return;\n const properties = value as Record<string, unknown>;\n for (const key of DROPPED_PROVIDER_PROPERTIES) Reflect.deleteProperty(properties, key);\n for (const [key, value] of Object.entries(properties)) {\n if (!key.startsWith('$') || typeof value !== 'string' || !/^https?:\\/\\//iu.test(value)) {\n continue;\n }\n try {\n const url = new URL(value);\n properties[key] = `${url.origin}${url.pathname}`;\n } catch {\n Reflect.deleteProperty(properties, key);\n }\n }\n}\n\n/**\n * `opt_out_capturing()` stops new captures but does not empty posthog's batch and retry queues,\n * which would still be sent (seen with 1.435.8). Revocation must not leak them, so they are cleared\n * here. The fields are internal to posthog-js: the version is pinned and a test runs this path\n * against the real library.\n */\nfunction discardPendingRequests(instance: PostHogInstanceLike): void {\n for (const name of ['_requestQueue', '_retryQueue']) {\n try {\n const queue = (instance as unknown as Record<string, { _queue?: unknown } | undefined>)[name];\n if (queue !== undefined && Array.isArray(queue._queue)) queue._queue = [];\n } catch {\n // Best effort; the gate in `before_send` already blocks new events.\n }\n }\n}\n\n/**\n * Removes `prefix` and `prefix_*`. Game ids never contain `_`, so another game whose id merely\n * starts with this one (`rail` / `rail-puzzle`) keeps its keys.\n */\nfunction removeSessionKeys(prefix: string): void {\n try {\n const storage = globalThis.sessionStorage;\n const keys: string[] = [];\n for (let index = 0; index < storage.length; index += 1) {\n const key = storage.key(index);\n if (key !== null && (key === prefix || key.startsWith(`${prefix}_`))) keys.push(key);\n }\n for (const key of keys) storage.removeItem(key);\n } catch {\n // Blocked storage: nothing was written there either.\n }\n}\n\nfunction hasBrowserGlobals(): boolean {\n return (\n typeof globalThis.window === 'object' &&\n typeof globalThis.document === 'object' &&\n typeof globalThis.navigator === 'object'\n );\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import type { PixelumeAnalyticsIssue } from '../../core/types.js';
2
+ /**
3
+ * Stable event catalog. Names are part of the public contract: dashboards are built on them, so
4
+ * they are only ever added, never renamed.
5
+ */
6
+ export declare const ANALYTICS_EVENTS: Readonly<{
7
+ readonly sessionStarted: "game_session_started";
8
+ readonly sessionEnded: "game_session_ended";
9
+ readonly levelStarted: "level_started";
10
+ readonly levelCompleted: "level_completed";
11
+ readonly levelFailed: "level_failed";
12
+ }>;
13
+ export declare const MAX_PROPERTIES = 25;
14
+ export declare const MAX_STRING_LENGTH = 200;
15
+ /** Metadata added by the SDK to every event; games cannot override it. */
16
+ export declare const METADATA_PROPERTIES: ReadonlySet<string>;
17
+ export type AnalyticsPropertyValue = string | number | boolean | null;
18
+ export type AnalyticsProperties = Record<string, AnalyticsPropertyValue>;
19
+ export type IssueReporter = (code: PixelumeAnalyticsIssue['code'], event: string | null, property?: string | null) => void;
20
+ export declare function isValidEventName(name: unknown): name is string;
21
+ export declare function isCatalogEvent(name: string): boolean;
22
+ /** A level is a finite number or a 1–64 character string; always sent as a string. */
23
+ export declare function normalizeLevel(level: unknown): string | null;
24
+ export declare function normalizeReason(reason: unknown): string | null;
25
+ /**
26
+ * Keeps only flat, small, non-sensitive values supplied by the game: snake_case keys, strings up
27
+ * to 200 characters, finite numbers, booleans and `null`; at most 25 keys. `undefined` is removed
28
+ * silently; anything else is dropped and reported. `protectedKeys` are set by the SDK itself.
29
+ */
30
+ export declare function sanitizeProperties(event: string, input: unknown, protectedKeys: ReadonlySet<string>, report: IssueReporter): AnalyticsProperties;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Stable event catalog. Names are part of the public contract: dashboards are built on them, so
3
+ * they are only ever added, never renamed.
4
+ */
5
+ export const ANALYTICS_EVENTS = Object.freeze({
6
+ sessionStarted: 'game_session_started',
7
+ sessionEnded: 'game_session_ended',
8
+ levelStarted: 'level_started',
9
+ levelCompleted: 'level_completed',
10
+ levelFailed: 'level_failed',
11
+ });
12
+ const CATALOG = new Set(Object.values(ANALYTICS_EVENTS));
13
+ /** snake_case, starts with a letter, 1–64 characters. Excludes provider names such as `$pageview`. */
14
+ const NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/u;
15
+ export const MAX_PROPERTIES = 25;
16
+ export const MAX_STRING_LENGTH = 200;
17
+ const MAX_LEVEL_LENGTH = 64;
18
+ /** Metadata added by the SDK to every event; games cannot override it. */
19
+ export const METADATA_PROPERTIES = new Set([
20
+ 'game_id',
21
+ 'game_version',
22
+ 'sdk_version',
23
+ 'session_id',
24
+ 'platform',
25
+ ]);
26
+ /** Keys the provider treats specially. */
27
+ const PROVIDER_PROPERTIES = new Set([
28
+ 'distinct_id',
29
+ 'token',
30
+ 'timestamp',
31
+ 'uuid',
32
+ 'event',
33
+ ]);
34
+ /**
35
+ * Best-effort guard against personal data and credentials. It complements, and does not replace,
36
+ * the rule that games never send them.
37
+ */
38
+ const SENSITIVE_KEYS = new Set([
39
+ 'email',
40
+ 'e_mail',
41
+ 'password',
42
+ 'secret',
43
+ 'token',
44
+ 'access_token',
45
+ 'refresh_token',
46
+ 'id_token',
47
+ 'authorization',
48
+ 'cookie',
49
+ 'ip',
50
+ 'ip_address',
51
+ 'phone',
52
+ 'cpf',
53
+ 'cnpj',
54
+ 'address',
55
+ 'name',
56
+ 'username',
57
+ 'user_name',
58
+ 'display_name',
59
+ 'full_name',
60
+ 'first_name',
61
+ 'last_name',
62
+ ]);
63
+ const SENSITIVE_SUFFIX = /_(?:email|password|secret|access_token|refresh_token|id_token)$/u;
64
+ const EMAIL_VALUE = /[^\s@]+@[^\s@]+\.[^\s@]+/u;
65
+ const CREDENTIAL_VALUE = /^(?:Bearer\s|eyJ[\w-]+\.[\w-]+\.)/u;
66
+ export function isValidEventName(name) {
67
+ return typeof name === 'string' && NAME_PATTERN.test(name);
68
+ }
69
+ export function isCatalogEvent(name) {
70
+ return CATALOG.has(name);
71
+ }
72
+ /** A level is a finite number or a 1–64 character string; always sent as a string. */
73
+ export function normalizeLevel(level) {
74
+ if (typeof level === 'number')
75
+ return Number.isFinite(level) ? String(level) : null;
76
+ if (typeof level !== 'string')
77
+ return null;
78
+ const trimmed = level.trim();
79
+ return trimmed.length === 0 || trimmed.length > MAX_LEVEL_LENGTH ? null : trimmed;
80
+ }
81
+ export function normalizeReason(reason) {
82
+ if (typeof reason !== 'string')
83
+ return null;
84
+ const trimmed = reason.trim();
85
+ return trimmed.length === 0 || trimmed.length > MAX_LEVEL_LENGTH ? null : trimmed;
86
+ }
87
+ /**
88
+ * Keeps only flat, small, non-sensitive values supplied by the game: snake_case keys, strings up
89
+ * to 200 characters, finite numbers, booleans and `null`; at most 25 keys. `undefined` is removed
90
+ * silently; anything else is dropped and reported. `protectedKeys` are set by the SDK itself.
91
+ */
92
+ export function sanitizeProperties(event, input, protectedKeys, report) {
93
+ const output = {};
94
+ if (input === undefined || input === null)
95
+ return output;
96
+ if (!isPlainObject(input)) {
97
+ report('invalid_property', event);
98
+ return output;
99
+ }
100
+ let count = 0;
101
+ for (const [key, value] of Object.entries(input)) {
102
+ if (value === undefined)
103
+ continue;
104
+ if (!NAME_PATTERN.test(key)) {
105
+ report('invalid_property', event);
106
+ continue;
107
+ }
108
+ if (METADATA_PROPERTIES.has(key) || PROVIDER_PROPERTIES.has(key) || protectedKeys.has(key)) {
109
+ report('reserved_property', event, key);
110
+ continue;
111
+ }
112
+ if (SENSITIVE_KEYS.has(key) || SENSITIVE_SUFFIX.test(key)) {
113
+ report('sensitive_property', event, key);
114
+ continue;
115
+ }
116
+ const normalized = normalizeValue(value);
117
+ if (normalized === INVALID) {
118
+ report('invalid_property', event, key);
119
+ continue;
120
+ }
121
+ if (typeof normalized === 'string' && looksSensitive(normalized)) {
122
+ report('sensitive_property', event, key);
123
+ continue;
124
+ }
125
+ if (count >= MAX_PROPERTIES) {
126
+ report('too_many_properties', event, key);
127
+ continue;
128
+ }
129
+ output[key] = normalized;
130
+ count += 1;
131
+ }
132
+ return output;
133
+ }
134
+ const INVALID = Symbol('invalid');
135
+ function normalizeValue(value) {
136
+ if (value === null || typeof value === 'boolean')
137
+ return value;
138
+ if (typeof value === 'number')
139
+ return Number.isFinite(value) ? value : INVALID;
140
+ if (typeof value === 'string')
141
+ return value.length <= MAX_STRING_LENGTH ? value : INVALID;
142
+ return INVALID;
143
+ }
144
+ function looksSensitive(value) {
145
+ return EMAIL_VALUE.test(value) || CREDENTIAL_VALUE.test(value);
146
+ }
147
+ function isPlainObject(value) {
148
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
149
+ return false;
150
+ const prototype = Object.getPrototypeOf(value);
151
+ return prototype === Object.prototype || prototype === null;
152
+ }
153
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../../src/modules/analytics/events.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PixelumeAnalyticsIssue } from '../../core/types.js';\n\n/**\n * Stable event catalog. Names are part of the public contract: dashboards are built on them, so\n * they are only ever added, never renamed.\n */\nexport const ANALYTICS_EVENTS = Object.freeze({\n sessionStarted: 'game_session_started',\n sessionEnded: 'game_session_ended',\n levelStarted: 'level_started',\n levelCompleted: 'level_completed',\n levelFailed: 'level_failed',\n} as const);\n\nconst CATALOG: ReadonlySet<string> = new Set(Object.values(ANALYTICS_EVENTS));\n/** snake_case, starts with a letter, 1–64 characters. Excludes provider names such as `$pageview`. */\nconst NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/u;\nexport const MAX_PROPERTIES = 25;\nexport const MAX_STRING_LENGTH = 200;\nconst MAX_LEVEL_LENGTH = 64;\n\n/** Metadata added by the SDK to every event; games cannot override it. */\nexport const METADATA_PROPERTIES: ReadonlySet<string> = new Set([\n 'game_id',\n 'game_version',\n 'sdk_version',\n 'session_id',\n 'platform',\n]);\n/** Keys the provider treats specially. */\nconst PROVIDER_PROPERTIES: ReadonlySet<string> = new Set([\n 'distinct_id',\n 'token',\n 'timestamp',\n 'uuid',\n 'event',\n]);\n/**\n * Best-effort guard against personal data and credentials. It complements, and does not replace,\n * the rule that games never send them.\n */\nconst SENSITIVE_KEYS: ReadonlySet<string> = new Set([\n 'email',\n 'e_mail',\n 'password',\n 'secret',\n 'token',\n 'access_token',\n 'refresh_token',\n 'id_token',\n 'authorization',\n 'cookie',\n 'ip',\n 'ip_address',\n 'phone',\n 'cpf',\n 'cnpj',\n 'address',\n 'name',\n 'username',\n 'user_name',\n 'display_name',\n 'full_name',\n 'first_name',\n 'last_name',\n]);\nconst SENSITIVE_SUFFIX = /_(?:email|password|secret|access_token|refresh_token|id_token)$/u;\nconst EMAIL_VALUE = /[^\\s@]+@[^\\s@]+\\.[^\\s@]+/u;\nconst CREDENTIAL_VALUE = /^(?:Bearer\\s|eyJ[\\w-]+\\.[\\w-]+\\.)/u;\n\nexport type AnalyticsPropertyValue = string | number | boolean | null;\nexport type AnalyticsProperties = Record<string, AnalyticsPropertyValue>;\nexport type IssueReporter = (\n code: PixelumeAnalyticsIssue['code'],\n event: string | null,\n property?: string | null,\n) => void;\n\nexport function isValidEventName(name: unknown): name is string {\n return typeof name === 'string' && NAME_PATTERN.test(name);\n}\n\nexport function isCatalogEvent(name: string): boolean {\n return CATALOG.has(name);\n}\n\n/** A level is a finite number or a 1–64 character string; always sent as a string. */\nexport function normalizeLevel(level: unknown): string | null {\n if (typeof level === 'number') return Number.isFinite(level) ? String(level) : null;\n if (typeof level !== 'string') return null;\n const trimmed = level.trim();\n return trimmed.length === 0 || trimmed.length > MAX_LEVEL_LENGTH ? null : trimmed;\n}\n\nexport function normalizeReason(reason: unknown): string | null {\n if (typeof reason !== 'string') return null;\n const trimmed = reason.trim();\n return trimmed.length === 0 || trimmed.length > MAX_LEVEL_LENGTH ? null : trimmed;\n}\n\n/**\n * Keeps only flat, small, non-sensitive values supplied by the game: snake_case keys, strings up\n * to 200 characters, finite numbers, booleans and `null`; at most 25 keys. `undefined` is removed\n * silently; anything else is dropped and reported. `protectedKeys` are set by the SDK itself.\n */\nexport function sanitizeProperties(\n event: string,\n input: unknown,\n protectedKeys: ReadonlySet<string>,\n report: IssueReporter,\n): AnalyticsProperties {\n const output: AnalyticsProperties = {};\n if (input === undefined || input === null) return output;\n if (!isPlainObject(input)) {\n report('invalid_property', event);\n return output;\n }\n let count = 0;\n for (const [key, value] of Object.entries(input)) {\n if (value === undefined) continue;\n if (!NAME_PATTERN.test(key)) {\n report('invalid_property', event);\n continue;\n }\n if (METADATA_PROPERTIES.has(key) || PROVIDER_PROPERTIES.has(key) || protectedKeys.has(key)) {\n report('reserved_property', event, key);\n continue;\n }\n if (SENSITIVE_KEYS.has(key) || SENSITIVE_SUFFIX.test(key)) {\n report('sensitive_property', event, key);\n continue;\n }\n const normalized = normalizeValue(value);\n if (normalized === INVALID) {\n report('invalid_property', event, key);\n continue;\n }\n if (typeof normalized === 'string' && looksSensitive(normalized)) {\n report('sensitive_property', event, key);\n continue;\n }\n if (count >= MAX_PROPERTIES) {\n report('too_many_properties', event, key);\n continue;\n }\n output[key] = normalized;\n count += 1;\n }\n return output;\n}\n\nconst INVALID = Symbol('invalid');\n\nfunction normalizeValue(value: unknown): AnalyticsPropertyValue | typeof INVALID {\n if (value === null || typeof value === 'boolean') return value;\n if (typeof value === 'number') return Number.isFinite(value) ? value : INVALID;\n if (typeof value === 'string') return value.length <= MAX_STRING_LENGTH ? value : INVALID;\n return INVALID;\n}\n\nfunction looksSensitive(value: string): boolean {\n return EMAIL_VALUE.test(value) || CREDENTIAL_VALUE.test(value);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const prototype = Object.getPrototypeOf(value) as unknown;\n return prototype === Object.prototype || prototype === null;\n}\n"]}
@@ -8,16 +8,35 @@ export interface PixelumePlayer {
8
8
  readonly displayName: string;
9
9
  readonly avatarUrl: string | null;
10
10
  }
11
- export type PixelumeAnonymousReason = 'visitor' | 'cancelled' | 'expired' | 'logged_out' | 'error';
11
+ /**
12
+ * Why there is no signed-in player:
13
+ * - `visitor`: no login to restore in this tab/browser (or restoration is off). Not proof that the
14
+ * central session ended.
15
+ * - `cancelled`: the player cancelled the login in the portal.
16
+ * - `expired`: a session that was running ended (renewal refused).
17
+ * - `logged_out`: `auth.logout()`.
18
+ * - `session_ended`: a cross-session restoration attempt found no valid central session
19
+ * (`login_required`); the hint was removed and no further automatic attempt is made.
20
+ * - `offline`: a cross-session restoration could not run or finish for lack of connectivity. The
21
+ * hint is kept; the next start (or `login()`) tries again. Local progress is untouched.
22
+ * - `error`: anything else; see `error`.
23
+ */
24
+ export type PixelumeAnonymousReason = 'visitor' | 'cancelled' | 'expired' | 'logged_out' | 'session_ended' | 'offline' | 'error';
25
+ /**
26
+ * `restoring: true` marks the silent cross-session attempt (`restoreAcrossSessions`): the page is
27
+ * leaving for a brief round trip without UI, or completing it. Interactive logins omit it.
28
+ */
12
29
  export type PixelumeAuthState = {
13
30
  readonly status: 'uninitialized';
14
31
  readonly player: null;
15
32
  } | {
16
33
  readonly status: 'redirecting';
17
34
  readonly player: null;
35
+ readonly restoring?: true;
18
36
  } | {
19
37
  readonly status: 'exchanging';
20
38
  readonly player: null;
39
+ readonly restoring?: true;
21
40
  } | {
22
41
  readonly status: 'anonymous';
23
42
  readonly player: null;