@omega.js/client 0.1.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 (72) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +874 -0
  3. package/dist/index.js +999 -0
  4. package/dist/modules/analytics.js +584 -0
  5. package/dist/modules/auth.js +469 -0
  6. package/dist/modules/bindings.js +319 -0
  7. package/dist/modules/device.js +282 -0
  8. package/dist/modules/dom.js +96 -0
  9. package/dist/modules/features.js +30 -0
  10. package/dist/modules/firestore.js +313 -0
  11. package/dist/modules/form-manager.js +1577 -0
  12. package/dist/modules/icon-core.js +226 -0
  13. package/dist/modules/icon-renderer.js +149 -0
  14. package/dist/modules/live-page.js +235 -0
  15. package/dist/modules/logger.js +36 -0
  16. package/dist/modules/motion.js +853 -0
  17. package/dist/modules/notifications.js +433 -0
  18. package/dist/modules/path-prefix.js +22 -0
  19. package/dist/modules/request.js +223 -0
  20. package/dist/modules/sentry.js +108 -0
  21. package/dist/modules/service-worker.js +237 -0
  22. package/dist/modules/storage.js +133 -0
  23. package/dist/modules/triggers.js +117 -0
  24. package/dist/modules/utilities.js +479 -0
  25. package/dist/modules/vert-document.js +354 -0
  26. package/dist/modules/verts.js +1133 -0
  27. package/dist/vendor/account/engine.js +182 -0
  28. package/dist/vendor/account/features.js +220 -0
  29. package/dist/vendor/account/index.js +53 -0
  30. package/dist/vendor/account/schema.js +272 -0
  31. package/dist/vendor/account/subscription.js +38 -0
  32. package/dist/vendor/analytics/adapters/ga4.js +26 -0
  33. package/dist/vendor/analytics/adapters/meta.js +26 -0
  34. package/dist/vendor/analytics/adapters/resolve.js +130 -0
  35. package/dist/vendor/analytics/adapters/tiktok.js +27 -0
  36. package/dist/vendor/analytics/catalog.js +908 -0
  37. package/dist/vendor/analytics/consent.js +49 -0
  38. package/dist/vendor/analytics/core.js +141 -0
  39. package/dist/vendor/analytics/identity.js +136 -0
  40. package/dist/vendor/analytics/index.js +170 -0
  41. package/dist/vendor/analytics/logger.js +40 -0
  42. package/dist/vendor/analytics/transports/browser.js +110 -0
  43. package/dist/vendor/monitoring/browser.js +207 -0
  44. package/dist/vendor/monitoring/core.js +180 -0
  45. package/dist/vendor/monitoring/logger.js +39 -0
  46. package/docs/architecture.md +59 -0
  47. package/docs/bindings.md +235 -0
  48. package/docs/build-system.md +32 -0
  49. package/docs/cdp-debugging.md +29 -0
  50. package/docs/code-patterns.md +96 -0
  51. package/docs/common-tasks.md +36 -0
  52. package/docs/dependencies.md +19 -0
  53. package/docs/index.md +159 -0
  54. package/docs/modules.md +180 -0
  55. package/docs/shared/agent-docs.md +89 -0
  56. package/docs/shared/analytics.md +612 -0
  57. package/docs/shared/brands.md +51 -0
  58. package/docs/shared/breaking-changes.md +497 -0
  59. package/docs/shared/config.md +1387 -0
  60. package/docs/shared/deploys.md +215 -0
  61. package/docs/shared/icons.md +201 -0
  62. package/docs/shared/local-dev.md +147 -0
  63. package/docs/shared/logging.md +202 -0
  64. package/docs/shared/monitoring.md +153 -0
  65. package/docs/shared/publishing.md +183 -0
  66. package/docs/shared/rulings.md +34 -0
  67. package/docs/shared/testing.md +147 -0
  68. package/docs/shared/theming.md +604 -0
  69. package/docs/shared/translation.md +291 -0
  70. package/docs/shared/updates.md +61 -0
  71. package/docs/testing.md +9 -0
  72. package/package.json +65 -0
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The consent seam.
3
+ *
4
+ * The gate answers ONE question — is this category granted right now — and the
5
+ * facade asks it once per provider before resolving. No UI, no storage, no
6
+ * region logic: stage B ([#383](https://github.com/Omega-JS-Stack/omega/issues/383))
7
+ * owns the banner and the timezone heuristic, and injects its state through
8
+ * `createConsentGate`.
9
+ *
10
+ * The provider function is read LIVE on every event, so a visitor who accepts
11
+ * mid-session is counted from that moment without anything re-configuring.
12
+ */
13
+
14
+ // The two categories every provider belongs to (adapters export their own).
15
+ const CATEGORIES = ['analytics', 'marketing'];
16
+
17
+ /**
18
+ * Build a consent gate from a state provider.
19
+ * @param {Function} providerFn - () => ({ analytics: boolean, marketing: boolean }).
20
+ * @returns {{ granted: Function, state: Function }}
21
+ */
22
+ function createConsentGate(providerFn) {
23
+ function state() {
24
+ const value = providerFn() || {};
25
+ const resolved = {};
26
+ for (const category of CATEGORIES) {
27
+ resolved[category] = value[category] === true;
28
+ }
29
+ return resolved;
30
+ }
31
+
32
+ return {
33
+ state,
34
+ /**
35
+ * Is this category granted?
36
+ * @param {string} category - 'analytics' | 'marketing'.
37
+ * @returns {boolean}
38
+ */
39
+ granted(category) {
40
+ return state()[category] === true;
41
+ },
42
+ };
43
+ }
44
+
45
+ // The default when no host injects one: everything granted. Stage B supplies
46
+ // the real gate on web; desktop/extension/backend have no banner to gate on.
47
+ const GRANT_ALL = createConsentGate(() => ({ analytics: true, marketing: true }));
48
+
49
+ module.exports = { CATEGORIES, createConsentGate, GRANT_ALL };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * core — the ONE place GA4 Measurement Protocol semantics live
3
+ * (C4 cp106b; Ian: "unify it and have it in one place").
4
+ *
5
+ * Pure functions, zero runtime assumptions: no DOM, no storage, no
6
+ * transport. @omega.js/client's browser engine and @omega.js/desktop's
7
+ * main-process lib both consume THIS module, so identity math and payload
8
+ * shape can never drift between surfaces again. It moved here from the client
9
+ * ([#382](https://github.com/Omega-JS-Stack/omega/issues/382)) so the server
10
+ * side reaches it without importing the frontend runtime.
11
+ *
12
+ * Cross-surface identity:
13
+ * deviceId = the host's stored id, its seed strategy, or a fresh uuidv4
14
+ * client_id = uuidv5(deviceId, namespace) — same device+surface, same GA client
15
+ * namespace = uuidv5(projectId, uuidv5.URL)
16
+ * user_id = uuidv5(firebaseUid, namespace) — same human, every surface
17
+ * Raw uids/device ids never leave the machine; without a namespace the
18
+ * user_id stays null (never the raw value).
19
+ *
20
+ * `deriveDeviceId` is where the chain starts and the only step that touches a
21
+ * host's world — so its persistence and its seed strategy are INJECTED
22
+ * ([#396](https://github.com/Omega-JS-Stack/omega/issues/396)), which keeps this
23
+ * module as assumption-free as the rest of it.
24
+ *
25
+ * CJS on purpose: desktop's Electron main process require()s it directly
26
+ * (via the package's dist exports); the ESM browser module imports it with
27
+ * standard interop.
28
+ */
29
+
30
+ const { v4: uuidv4, v5: uuidv5 } = require('uuid');
31
+
32
+ const GA_ENDPOINT = 'https://www.google-analytics.com/mp/collect';
33
+
34
+ /**
35
+ * The stable per-install device id every surface's client_id is hashed from —
36
+ * ONE derivation, on the `createRequest(deps)` mold: what differs per target is
37
+ * WHERE it persists and WHAT it seeds from, and both are handed in.
38
+ *
39
+ * The walk is stored → seed → uuidv4. Storage wins so an id survives whatever
40
+ * the seed does next (desktop stays put across a NIC swap or a VPN); the seed is
41
+ * what gives a wiped install continuity (desktop's first non-internal MAC), and
42
+ * a host with none — a browser, where nothing about the machine is readable —
43
+ * generates one and persists it. The floor is the `uuid` package's `v4`, which
44
+ * yields a REAL uuid in every runtime this ships to: it uses the platform's
45
+ * `crypto.randomUUID` where that exists and `getRandomValues` where it does not
46
+ * (an insecure origin), so no surface ever falls back to a random-looking string.
47
+ *
48
+ * @param {object} deps - The host's world.
49
+ * @param {function(): string|null} deps.get - Read the persisted id.
50
+ * @param {function(string): void} deps.set - Persist a freshly derived id.
51
+ * @param {function(): string|null} [deps.seed] - The target's id source, asked
52
+ * only when nothing is stored. Anything falsy falls through to the uuid.
53
+ * @returns {string} The raw device id — never sent anywhere as-is.
54
+ */
55
+ function deriveDeviceId(deps) {
56
+ if (typeof deps?.get !== 'function' || typeof deps?.set !== 'function') {
57
+ throw new Error('deriveDeviceId requires get and set deps');
58
+ }
59
+
60
+ const stored = deps.get();
61
+
62
+ if (stored) {
63
+ return stored;
64
+ }
65
+
66
+ const deviceId = (deps.seed ? deps.seed() : null) || uuidv4();
67
+
68
+ deps.set(deviceId);
69
+
70
+ return deviceId;
71
+ }
72
+
73
+ /** uuidv5 namespace for a project — null in, null out. */
74
+ function deriveNamespace(projectId) {
75
+ return projectId ? uuidv5(String(projectId), uuidv5.URL) : null;
76
+ }
77
+
78
+ /** Stable GA client_id: hashed into the namespace when one exists. */
79
+ function deriveClientId(deviceId, namespace) {
80
+ return namespace ? uuidv5(String(deviceId), namespace) : deviceId;
81
+ }
82
+
83
+ /** GA user_id from a raw uid — no namespace → null (raw uids never ship). */
84
+ function deriveUserId(uid, namespace) {
85
+ return (uid && namespace) ? uuidv5(String(uid), namespace) : null;
86
+ }
87
+
88
+ /** GA4 event names: letters/digits/underscore, ≤40 chars, no edge underscores. */
89
+ function normalizeEventName(name) {
90
+ if (!name || typeof name !== 'string') {
91
+ return null;
92
+ }
93
+
94
+ return name
95
+ .replace(/[^a-zA-Z0-9_]/g, '_')
96
+ .replace(/^_+|_+$/g, '')
97
+ .replace(/_+/g, '_')
98
+ .slice(0, 40);
99
+ }
100
+
101
+ /** GA4 user_properties wrapping: { plan: 'pro' } → { plan: { value: 'pro' } }. */
102
+ function wrapUserProperties(properties = {}) {
103
+ const wrapped = {};
104
+ for (const [key, value] of Object.entries(properties)) {
105
+ wrapped[key] = { value };
106
+ }
107
+ return wrapped;
108
+ }
109
+
110
+ /** Measurement Protocol collect URL. */
111
+ function buildCollectUrl(measurementId, secret) {
112
+ return `${GA_ENDPOINT}?measurement_id=${encodeURIComponent(measurementId)}&api_secret=${encodeURIComponent(secret)}`;
113
+ }
114
+
115
+ /**
116
+ * Measurement Protocol payload. userId/userProperties are omitted when
117
+ * empty — GA rejects nulls, and an empty user_properties block is noise.
118
+ */
119
+ function buildPayload({ clientId, userId = null, userProperties = {}, eventName, params = {} }) {
120
+ return {
121
+ client_id: clientId,
122
+ ...(userId ? { user_id: userId } : {}),
123
+ ...(Object.keys(userProperties).length ? { user_properties: userProperties } : {}),
124
+ events: [{
125
+ name: eventName,
126
+ params,
127
+ }],
128
+ };
129
+ }
130
+
131
+ module.exports = {
132
+ GA_ENDPOINT,
133
+ deriveDeviceId,
134
+ deriveNamespace,
135
+ deriveClientId,
136
+ deriveUserId,
137
+ normalizeEventName,
138
+ wrapUserProperties,
139
+ buildCollectUrl,
140
+ buildPayload,
141
+ };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * identity — the match keys a signed-in visitor is recognized BY, normalized the
3
+ * way each platform's own spec demands
4
+ * ([#328](https://github.com/Omega-JS-Stack/omega/issues/328)).
5
+ *
6
+ * Every ad platform matches on a SHA-256 of a NORMALIZED value, and normalized
7
+ * is not one rule. Meta's advanced matching wants a phone as bare digits —
8
+ * country code included, no `+`, no punctuation — while TikTok's pixel wants the
9
+ * E.164 form WITH the `+`. Same person, two digests, so the normalizer is per
10
+ * provider and only the hash is shared. Email is the one value everybody
11
+ * normalizes alike: trimmed and lowercased.
12
+ *
13
+ * RAW PII NEVER LEAVES: a caller hashes here and hands the pixel the digest.
14
+ * `external_id` is not PII, and it is per provider like the rest
15
+ * ([#410](https://github.com/Omega-JS-Stack/omega/issues/410)): META's spec only
16
+ * RECOMMENDS hashing and its own Pixel example passes a bare id, so Meta gets
17
+ * the RAW uid — exactly what the SERVER sends as `identity.externalId`
18
+ * (@omega.js/backend's `libraries/analytics/match-data.js`). TIKTOK's Events API
19
+ * REQUIRES the digest, so both halves send the SHA-256 of that same uid,
20
+ * normalized by `normalizeExternalId()` below. Either way the browser half and
21
+ * the server half of one person link only because both carry the same value.
22
+ *
23
+ * Pure and runtime-neutral like the rest of the package's pieces: no DOM, no
24
+ * transport, no state. CJS, so the backend can require() it and the browser
25
+ * bundles import it with standard interop.
26
+ */
27
+
28
+ // The node digest's specifier, held in a VARIABLE on purpose: the web bundle is
29
+ // built for the browser, where a builtin has no home, and esbuild fails the
30
+ // whole build on a literally-named builtin it cannot resolve. Read through a
31
+ // variable it is left alone — and the branch below only ever runs off-page.
32
+ const NODE_CRYPTO = 'crypto';
33
+
34
+ /**
35
+ * Is this runtime node? The node digest is chosen by the RUNTIME and never by
36
+ * `typeof require`: a bundler rewrites that name to a shim of its own, which IS
37
+ * a function and throws the moment it is called with a builtin — a rejection
38
+ * thrown at a visitor, exactly where this module promises a quiet null.
39
+ * @returns {boolean}
40
+ */
41
+ function isNode() {
42
+ return typeof process !== 'undefined' && !!process.versions && !!process.versions.node;
43
+ }
44
+
45
+ /**
46
+ * SHA-256 hex — the digest every platform's match spec asks for.
47
+ *
48
+ * A page's only hash is `crypto.subtle`, which is asynchronous and absent on a
49
+ * non-secure origin; node's `crypto` covers the runtimes that have no secure
50
+ * context (the test runner, a Cloud Function, a plain-http page).
51
+ *
52
+ * @param {string} [value] - The ALREADY-normalized value.
53
+ * @returns {Promise<string|null>} Lowercase hex, or null when there is nothing
54
+ * to hash and when no digest exists to hash it with.
55
+ */
56
+ async function sha256(value) {
57
+ if (!value) {
58
+ return null;
59
+ }
60
+
61
+ const subtle = globalThis.crypto?.subtle;
62
+
63
+ if (subtle) {
64
+ const digest = await subtle.digest('SHA-256', new TextEncoder().encode(value));
65
+
66
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
67
+ }
68
+
69
+ // A page with no secure context and no node under it has no digest at all,
70
+ // which means no identity — and silence, never a raise at a visitor (#306).
71
+ if (!isNode()) {
72
+ return null;
73
+ }
74
+
75
+ try {
76
+ return require(NODE_CRYPTO).createHash('sha256').update(value).digest('hex');
77
+ } catch (e) {
78
+ // A bundler that shims `require` throws here rather than resolving a
79
+ // builtin. Same answer as every other missing digest: no match key, no
80
+ // noise — the promise above holds in every shape this module is built into.
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Email, normalized the one way every platform agrees on: trimmed, lowercased.
87
+ * @param {string} [email]
88
+ * @returns {string} The normalized address, or '' when there is none.
89
+ */
90
+ function normalizeEmail(email) {
91
+ return `${email || ''}`.trim().toLowerCase();
92
+ }
93
+
94
+ /**
95
+ * External id: trimmed, and NOTHING else. TikTok's advanced-matching table is
96
+ * the only spec that states the rule ("Trim any leading and trailing spaces
97
+ * before hashing and ensure you are consistent with the External ID used"), and
98
+ * an id is case-SENSITIVE — lowercasing it the way an email is normalized would
99
+ * hand TikTok a digest of a uid that never existed (#410).
100
+ * @param {string} [uid] - The account's uid.
101
+ * @returns {string} The normalized id, or '' when there is none.
102
+ */
103
+ function normalizeExternalId(uid) {
104
+ return `${uid || ''}`.trim();
105
+ }
106
+
107
+ /**
108
+ * Phone for META: digits only, country code included, no `+` and no punctuation
109
+ * (Meta's advanced-matching spec hashes the bare number).
110
+ * @param {string} [phone] - Any written form; Auth's `phoneNumber` is E.164.
111
+ * @returns {string} The digits, or '' when there is no number.
112
+ */
113
+ function metaPhone(phone) {
114
+ return `${phone || ''}`.replace(/\D/g, '');
115
+ }
116
+
117
+ /**
118
+ * Phone for TIKTOK: the same digits in E.164, with the leading `+` its pixel
119
+ * spec requires. Nothing to normalize stays empty rather than becoming a lone
120
+ * `+`, which would hash to a match key shared by every phoneless account.
121
+ * @param {string} [phone] - Any written form; Auth's `phoneNumber` is E.164.
122
+ * @returns {string} The E.164 number, or '' when there is no number.
123
+ */
124
+ function tiktokPhone(phone) {
125
+ const digits = metaPhone(phone);
126
+
127
+ return digits ? `+${digits}` : '';
128
+ }
129
+
130
+ module.exports = {
131
+ sha256,
132
+ normalizeEmail,
133
+ normalizeExternalId,
134
+ metaPhone,
135
+ tiktokPhone,
136
+ };
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @omega.js/analytics — the ONE analytics consumption surface.
3
+ *
4
+ * Client code and backend code alike call `analytics.event('<canonical>', params)`
5
+ * ([#328](https://github.com/Omega-JS-Stack/omega/issues/328) §Architecture).
6
+ * What differs underneath — page globals on a browser, the Measurement
7
+ * Protocol and the conversion APIs on a server — is the TRANSPORT, injected by
8
+ * the host and invisible to callers.
9
+ *
10
+ * One fire walks three steps per provider:
11
+ * 1. consent — a blocked category's providers never even resolve
12
+ * 2. adapter — the catalog mapping, or null when the provider has none
13
+ * 3. transport — the host's seam; a missing global is a silent no-op (#306)
14
+ *
15
+ * In development the whole walk prints as ONE line per fire, which is the job
16
+ * `setupTrackingInterceptors()` did in web core's `libs/dev.js` (they retired
17
+ * with the rewire).
18
+ *
19
+ * Environment is INJECTED, never sniffed — the same seam the rest of the
20
+ * ecosystem uses (`@omega.js/client`'s `config.environment === 'development'`).
21
+ * The default is 'production': an unconfigured runtime must never throw an
22
+ * event-name error at a visitor.
23
+ */
24
+
25
+ const { CATALOG, entryFor } = require('./catalog.js');
26
+ const { createConsentGate, GRANT_ALL, CATEGORIES } = require('./consent.js');
27
+ const { createLogger } = require('./logger.js');
28
+ const core = require('./core.js');
29
+ const identity = require('./identity.js');
30
+ const ga4 = require('./adapters/ga4.js');
31
+ const meta = require('./adapters/meta.js');
32
+ const tiktok = require('./adapters/tiktok.js');
33
+ const browser = require('./transports/browser.js');
34
+
35
+ const logger = createLogger('events');
36
+
37
+ // Every provider, in fire order. A new platform joins here and in the catalog.
38
+ const ADAPTERS = [ga4, meta, tiktok];
39
+
40
+ const DEFAULTS = {
41
+ // No transport = resolve and log, deliver nothing. A host that never calls
42
+ // configure() is inert rather than guessing at page globals.
43
+ transport: null,
44
+ consent: GRANT_ALL,
45
+ context: {},
46
+ environment: 'production',
47
+ };
48
+
49
+ let state = { ...DEFAULTS };
50
+
51
+ /**
52
+ * Inject the host's seams. Merges, so a host can wire the transport at boot
53
+ * and swap the consent gate later.
54
+ *
55
+ * @param {object} [options]
56
+ * @param {object} [options.transport] - { send(descriptor) => boolean }.
57
+ * @param {object} [options.consent] - A gate from `createConsentGate`.
58
+ * @param {object} [options.context] - { attribution, consent, runtime } handed to adapters.
59
+ * @param {string} [options.environment] - 'development' | 'production'.
60
+ * @returns {object} The resolved state.
61
+ */
62
+ function configure(options = {}) {
63
+ state = { ...state, ...options };
64
+ return { ...state };
65
+ }
66
+
67
+ /**
68
+ * Is this runtime in development? Mirrors the client's `config.environment` seam.
69
+ * @returns {boolean}
70
+ */
71
+ function isDevelopment() {
72
+ return state.environment === 'development';
73
+ }
74
+
75
+ // Walk one provider. Returns { provider, outcome, descriptor? } — never throws.
76
+ function fire(adapter, canonicalName, params, { eventId, providers }) {
77
+ const provider = adapter.provider;
78
+
79
+ // Selection comes before consent: a provider this half does not own was never
80
+ // going to be asked, whatever the visitor consented to (the same order the
81
+ // backend's deliverConversion walks).
82
+ if (providers && !providers.includes(provider)) {
83
+ return { provider, outcome: 'skipped (not selected)' };
84
+ }
85
+
86
+ if (!state.consent.granted(adapter.CONSENT_CATEGORY)) {
87
+ return { provider, outcome: `skipped (consent: ${adapter.CONSENT_CATEGORY})` };
88
+ }
89
+
90
+ const descriptor = adapter.resolve(canonicalName, params, state.context);
91
+ if (!descriptor) {
92
+ return { provider, outcome: 'skipped (no mapping)' };
93
+ }
94
+
95
+ // The dedupe id rides ON the descriptor, which is what a transport executes:
96
+ // Meta takes it as the pixel's `eventID` option and TikTok as `event_id`, so
97
+ // a browser half and a server half of the same conversion collapse into one.
98
+ if (eventId) {
99
+ descriptor.eventId = eventId;
100
+ }
101
+
102
+ if (!state.transport) {
103
+ return { provider, outcome: 'skipped (no transport)', descriptor };
104
+ }
105
+
106
+ // A transport that returns false could not deliver (a blocked or missing
107
+ // page global). It is a no-op by contract, so the dev log is its only trace.
108
+ const delivered = state.transport.send(descriptor);
109
+
110
+ return { provider, outcome: delivered === false ? 'blocked (no global)' : 'sent', descriptor };
111
+ }
112
+
113
+ /**
114
+ * Fire a canonical event across every provider that maps it.
115
+ *
116
+ * An unknown name is a PROGRAMMER error — a typo must not silently cost a
117
+ * conversion — so it throws in development and is logged-and-skipped in
118
+ * production, where throwing would take the customer's action with it.
119
+ *
120
+ * @param {string} canonicalName - A name declared in the catalog.
121
+ * @param {object} [params] - The canonical params for that event.
122
+ * @param {object} [options] - The fire's own options.
123
+ * @param {string} [options.eventId] - The platform dedupe id, for an event whose
124
+ * other half fires server-side. Both halves MUST name the same string.
125
+ * @param {string[]} [options.providers] - Restrict the fire to these providers.
126
+ * For a `placement: 'both'` event whose halves do not all deduplicate — this
127
+ * half names the providers it owns. Absent = every provider the catalog maps.
128
+ * @returns {{ event: string, results: object[] }} One result per provider.
129
+ */
130
+ function event(canonicalName, params = {}, options = {}) {
131
+ const entry = entryFor(canonicalName);
132
+
133
+ if (!entry) {
134
+ if (isDevelopment()) {
135
+ throw new Error(`Unknown analytics event "${canonicalName}" — every event is declared in the catalog (@omega.js/analytics/catalog)`);
136
+ }
137
+
138
+ logger.warn(`Unknown event "${canonicalName}" — not in the catalog, skipped`);
139
+ return { event: canonicalName, results: [] };
140
+ }
141
+
142
+ const results = ADAPTERS.map((adapter) => fire(adapter, canonicalName, params, options));
143
+
144
+ if (isDevelopment()) {
145
+ logger.log(`${canonicalName} → ${results.map((result) => `${result.provider} ${result.outcome}`).join(', ')}`);
146
+ }
147
+
148
+ return { event: canonicalName, results };
149
+ }
150
+
151
+ module.exports = {
152
+ // The consumption surface
153
+ event,
154
+ configure,
155
+ isDevelopment,
156
+
157
+ // The pure pieces, for the hosts that need one without the facade
158
+ catalog: CATALOG,
159
+ entryFor,
160
+ adapters: { ga4, meta, tiktok },
161
+ transports: { browser },
162
+ core,
163
+ // Identity is not an event, so it never walks the catalog: a host hashes the
164
+ // signed-in visitor's match keys with these and sets them on its own pixels.
165
+ identity,
166
+
167
+ // The consent seam
168
+ createConsentGate,
169
+ CONSENT_CATEGORIES: CATEGORIES,
170
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The analytics package's log tag — the ONE identity tag every OMEGA surface
3
+ * prints: `[@omega.js/analytics:<module>]` ([#12](https://github.com/Omega-JS-Stack/omega/issues/12)).
4
+ *
5
+ * This package runs on BOTH surfaces (a browser bundle and a Cloud Function),
6
+ * so it follows the runtime shape: no timestamp — devtools and Cloud Logging
7
+ * both stamp their own lines. The twin of @omega.js/client's createLogger,
8
+ * emitting this package's segment.
9
+ *
10
+ * CJS like the rest of the package: the desktop main process and the backend
11
+ * require() it, the browser bundles import it with standard interop.
12
+ */
13
+
14
+ // The package segment — this file IS @omega.js/analytics, so it is a literal.
15
+ const PACKAGE = '@omega.js/analytics';
16
+
17
+ /**
18
+ * Create a tagged console for one module, e.g. createLogger('events').
19
+ * @param {string} module - The module identity segment.
20
+ * @returns {object} A console-shaped logger whose calls carry the tag.
21
+ */
22
+ function createLogger(module) {
23
+ const tag = `[${PACKAGE}:${module}]`;
24
+
25
+ // GETTERS returning a BOUND console method, not wrapper arrows: devtools
26
+ // attributes a line to the frame that called console, so a wrapper would make
27
+ // every line read as coming from this file. Resolution stays at ACCESS time,
28
+ // so a test (or a consumer) that swaps console[method] still sees its own stub.
29
+ const logger = { tag };
30
+ for (const method of ['log', 'info', 'warn', 'error', 'debug']) {
31
+ Object.defineProperty(logger, method, {
32
+ get: () => console[method].bind(console, tag),
33
+ enumerable: true,
34
+ });
35
+ }
36
+
37
+ return logger;
38
+ }
39
+
40
+ module.exports = { createLogger };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The browser transport — executes descriptors against the page's own pixel
3
+ * globals, absorbing the guard semantics of
4
+ * [#306](https://github.com/Omega-JS-Stack/omega/issues/306).
5
+ *
6
+ * `gtag`, `fbq` and `ttq` are page-level snippets, and an ad blocker does not
7
+ * stub them: it keeps them from ever being defined, so a BARE call throws a
8
+ * ReferenceError. Every one of these calls sits in front of the thing the
9
+ * customer just pressed, so that throw takes the action with it (the bug
10
+ * [#283](https://github.com/Omega-JS-Stack/omega/issues/283) fixed inside the
11
+ * billing card, found again in 19 other files).
12
+ *
13
+ * `typeof` against an undeclared name is the one check that does not throw, and
14
+ * every provider is checked on its own: blockers are per-list, so a page that
15
+ * lost Meta still counts Google. A missing or blocked global is a SILENT no-op,
16
+ * never a throw — the return value is the only trace, so the facade's dev log
17
+ * can tell "delivered" from "blocked" without anything reaching a visitor.
18
+ */
19
+
20
+ // Google Analytics 4 — one command function; the event command is 'event'.
21
+ function sendGoogle(descriptor) {
22
+ if (typeof gtag !== 'function') {
23
+ return false;
24
+ }
25
+
26
+ gtag('event', descriptor.name, descriptor.payload);
27
+ return true;
28
+ }
29
+
30
+ // Facebook Pixel — one command function; the kind picks the command, which is
31
+ // the whole point of the catalog's standard/custom distinction.
32
+ //
33
+ // A descriptor carrying an `eventId` is one half of a conversion whose other
34
+ // half fires server-side: the Pixel's fourth argument is where its `eventID`
35
+ // goes, and Meta deduplicates on the (event_name, event_id) PAIR. It is passed
36
+ // only when there is one — an `{ eventID: undefined }` option object reads to
37
+ // the Pixel as an id we tried and failed to send.
38
+ function sendMeta(descriptor) {
39
+ if (typeof fbq !== 'function') {
40
+ return false;
41
+ }
42
+
43
+ const command = descriptor.kind === 'custom' ? 'trackCustom' : 'track';
44
+
45
+ if (descriptor.eventId) {
46
+ fbq(command, descriptor.name, descriptor.payload, { eventID: descriptor.eventId });
47
+ } else {
48
+ fbq(command, descriptor.name, descriptor.payload);
49
+ }
50
+
51
+ return true;
52
+ }
53
+
54
+ // TikTok Pixel — an object of methods, so the method gets its own check. Its
55
+ // dedupe key is `event_id` in the third argument (the Events API half sends the
56
+ // same string).
57
+ //
58
+ // A descriptor carrying a `method` is the signal TikTok manages ITSELF rather
59
+ // than exposing as a trackable name: the page view, whose documented surface is
60
+ // `ttq.page()` and nothing else ([#409](https://github.com/Omega-JS-Stack/omega/issues/409)).
61
+ // It takes no name and no payload — the pixel reads the page — and a build that
62
+ // does not have the method is the same silent no-op a blocked global is (#306).
63
+ function sendTikTok(descriptor) {
64
+ if (typeof ttq === 'undefined') {
65
+ return false;
66
+ }
67
+
68
+ if (descriptor.method) {
69
+ if (typeof ttq[descriptor.method] !== 'function') {
70
+ return false;
71
+ }
72
+
73
+ ttq[descriptor.method]();
74
+ return true;
75
+ }
76
+
77
+ if (typeof ttq.track !== 'function') {
78
+ return false;
79
+ }
80
+
81
+ if (descriptor.eventId) {
82
+ ttq.track(descriptor.name, descriptor.payload, { event_id: descriptor.eventId });
83
+ } else {
84
+ ttq.track(descriptor.name, descriptor.payload);
85
+ }
86
+
87
+ return true;
88
+ }
89
+
90
+ const SENDERS = {
91
+ ga4: sendGoogle,
92
+ meta: sendMeta,
93
+ tiktok: sendTikTok,
94
+ };
95
+
96
+ /**
97
+ * Execute one descriptor against the page globals.
98
+ * @param {object} descriptor - { provider, name, kind, payload, userData }.
99
+ * @returns {boolean} true when the provider's global was there to take it.
100
+ */
101
+ function send(descriptor) {
102
+ const sender = SENDERS[descriptor.provider];
103
+ if (!sender) {
104
+ return false;
105
+ }
106
+
107
+ return sender(descriptor);
108
+ }
109
+
110
+ module.exports = { send };