@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.
- package/LICENSE +98 -0
- package/README.md +874 -0
- package/dist/index.js +999 -0
- package/dist/modules/analytics.js +584 -0
- package/dist/modules/auth.js +469 -0
- package/dist/modules/bindings.js +319 -0
- package/dist/modules/device.js +282 -0
- package/dist/modules/dom.js +96 -0
- package/dist/modules/features.js +30 -0
- package/dist/modules/firestore.js +313 -0
- package/dist/modules/form-manager.js +1577 -0
- package/dist/modules/icon-core.js +226 -0
- package/dist/modules/icon-renderer.js +149 -0
- package/dist/modules/live-page.js +235 -0
- package/dist/modules/logger.js +36 -0
- package/dist/modules/motion.js +853 -0
- package/dist/modules/notifications.js +433 -0
- package/dist/modules/path-prefix.js +22 -0
- package/dist/modules/request.js +223 -0
- package/dist/modules/sentry.js +108 -0
- package/dist/modules/service-worker.js +237 -0
- package/dist/modules/storage.js +133 -0
- package/dist/modules/triggers.js +117 -0
- package/dist/modules/utilities.js +479 -0
- package/dist/modules/vert-document.js +354 -0
- package/dist/modules/verts.js +1133 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/consent.js +49 -0
- package/dist/vendor/analytics/core.js +141 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/analytics/index.js +170 -0
- package/dist/vendor/analytics/logger.js +40 -0
- package/dist/vendor/analytics/transports/browser.js +110 -0
- package/dist/vendor/monitoring/browser.js +207 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/docs/architecture.md +59 -0
- package/docs/bindings.md +235 -0
- package/docs/build-system.md +32 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/code-patterns.md +96 -0
- package/docs/common-tasks.md +36 -0
- package/docs/dependencies.md +19 -0
- package/docs/index.md +159 -0
- package/docs/modules.md +180 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +9 -0
- package/package.json +65 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* USER_SCHEMA — the canonical OMEGA user/account schema (pure data, no logic).
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from @omega.js/backend's src/manager/helpers/user.js, which is
|
|
5
|
+
* the authoritative shape (@omega.js/client's DEFAULT_ACCOUNT had drifted from it).
|
|
6
|
+
*
|
|
7
|
+
* Each leaf field is { type, default, nullable }
|
|
8
|
+
* Special keys:
|
|
9
|
+
* $passthrough — preserve all existing keys from input, don't strip unknowns
|
|
10
|
+
* $template — shape applied to every dynamic key in a $passthrough object
|
|
11
|
+
* '$template' — (string value) reference to parent's $template
|
|
12
|
+
* '$timestamp' — shorthand for { timestamp, timestampUNIX } defaulting to epoch
|
|
13
|
+
* '$timestamp:now' — same but defaults to current time
|
|
14
|
+
* '$uuid', '$randomId', '$apiKey', '$oldDate' — resolved at runtime (generators
|
|
15
|
+
* are injected by the host — see engine.js; absent generators resolve to null)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One attribution TOUCH — a single visit's campaign context, shared by
|
|
20
|
+
* `attribution.first` and `attribution.last`
|
|
21
|
+
* ([#384](https://github.com/Omega-JS-Stack/omega/issues/384)). `tags` holds the
|
|
22
|
+
* utm set and `clickIds` the ad-platform click ids (fbclid/gclid/…), both
|
|
23
|
+
* passthrough because the platforms keep adding params. The schema is read-only
|
|
24
|
+
* to the engine, so one object serves both slots.
|
|
25
|
+
*/
|
|
26
|
+
const ATTRIBUTION_TOUCH = {
|
|
27
|
+
tags: { $passthrough: true },
|
|
28
|
+
clickIds: { $passthrough: true },
|
|
29
|
+
referrer: { type: 'string', default: null, nullable: true },
|
|
30
|
+
url: { type: 'string', default: null, nullable: true },
|
|
31
|
+
page: { type: 'string', default: null, nullable: true },
|
|
32
|
+
timestamp: { type: 'string', default: null, nullable: true },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const USER_SCHEMA = {
|
|
36
|
+
auth: {
|
|
37
|
+
uid: { type: 'string', default: null, nullable: true },
|
|
38
|
+
email: { type: 'string', default: null, nullable: true },
|
|
39
|
+
temporary: { type: 'boolean', default: false },
|
|
40
|
+
},
|
|
41
|
+
subscription: {
|
|
42
|
+
product: {
|
|
43
|
+
id: { type: 'string', default: 'basic' },
|
|
44
|
+
name: { type: 'string', default: 'Basic' },
|
|
45
|
+
},
|
|
46
|
+
status: { type: 'string', default: 'active' },
|
|
47
|
+
expires: '$timestamp',
|
|
48
|
+
trial: {
|
|
49
|
+
claimed: { type: 'boolean', default: false },
|
|
50
|
+
expires: '$timestamp',
|
|
51
|
+
// How the trial ENDED: 'converted' | 'lapsed' | null (still running, or never
|
|
52
|
+
// answered). `claimed` says only that a trial happened — a converted trial and a
|
|
53
|
+
// lapsed one carry identical dates — so this is the one stored conversion signal.
|
|
54
|
+
// Stamped by the trial-lapse sweep once the provider confirms which it was.
|
|
55
|
+
outcome: { type: 'string', default: null, nullable: true },
|
|
56
|
+
},
|
|
57
|
+
cancellation: {
|
|
58
|
+
pending: { type: 'boolean', default: false },
|
|
59
|
+
date: '$timestamp',
|
|
60
|
+
},
|
|
61
|
+
payment: {
|
|
62
|
+
provider: { type: 'string', default: null, nullable: true },
|
|
63
|
+
orderId: { type: 'string', default: null, nullable: true },
|
|
64
|
+
resourceId: { type: 'string', default: null, nullable: true },
|
|
65
|
+
frequency: { type: 'string', default: null, nullable: true },
|
|
66
|
+
price: { type: 'number', default: 0 },
|
|
67
|
+
startDate: '$timestamp',
|
|
68
|
+
updatedBy: {
|
|
69
|
+
event: {
|
|
70
|
+
name: { type: 'string', default: null, nullable: true },
|
|
71
|
+
id: { type: 'string', default: null, nullable: true },
|
|
72
|
+
},
|
|
73
|
+
date: '$timestamp',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
// The discount riding the subscription right now
|
|
77
|
+
// ([#325](https://github.com/Omega-JS-Stack/omega/issues/325)). Shaped as a
|
|
78
|
+
// discount-codes validate() RESULT — the one shape the whole payment stack
|
|
79
|
+
// already speaks — so the billing card reads exactly what the apply route
|
|
80
|
+
// wrote, and neither side learns a second spelling. `valid: false` (the
|
|
81
|
+
// default) IS "no discount": absent and denied read the same everywhere.
|
|
82
|
+
//
|
|
83
|
+
// Both shapes are always present and default to 0, unlike a fresh validate()
|
|
84
|
+
// result which omits the one it is not: this node is MERGED onto a document
|
|
85
|
+
// that may already carry a discount, and a percent claim landing on a stored
|
|
86
|
+
// amount would otherwise be read as the older, wrong number.
|
|
87
|
+
//
|
|
88
|
+
// `source` is what makes it safe to read as a CLAIM. Today only the winback
|
|
89
|
+
// claim writes this node; a code typed at checkout will set the same node
|
|
90
|
+
// when checkout starts writing it, and a winback pitch suppressed by someone
|
|
91
|
+
// else's promo is an offer silently withheld — so the node says which system
|
|
92
|
+
// applied it ('winback' | 'checkout'), and only 'winback' is the claimed
|
|
93
|
+
// signal the cancel flow gates on.
|
|
94
|
+
//
|
|
95
|
+
// `resourceId` is the subscription the discount was applied TO, stamped at
|
|
96
|
+
// claim time ([#333](https://github.com/Omega-JS-Stack/omega/issues/333)).
|
|
97
|
+
// It is what makes the node clearable: the unified webhook write carries no
|
|
98
|
+
// discount key, so a merge preserves this node forever, and a customer who
|
|
99
|
+
// churned and resubscribed carried a spent claim into the NEW subscription,
|
|
100
|
+
// where `source: 'winback'` reads as "already claimed" and silently retires
|
|
101
|
+
// an offer the backend would grant again. The webhook pipeline compares the
|
|
102
|
+
// stamp against the subscription each event is about and CLEARS the node
|
|
103
|
+
// (back to these defaults) on a mismatch: a new subscription is a clean
|
|
104
|
+
// slate. An unstamped node (written before the stamp existed) is read as
|
|
105
|
+
// belonging to the subscription it is found on, because nothing can prove
|
|
106
|
+
// otherwise and no live discount is ever taken away on a guess, and the
|
|
107
|
+
// pipeline stamps it there, so it clears on the next resubscribe like any
|
|
108
|
+
// other.
|
|
109
|
+
discount: {
|
|
110
|
+
valid: { type: 'boolean', default: false },
|
|
111
|
+
code: { type: 'string', default: null, nullable: true },
|
|
112
|
+
percent: { type: 'number', default: 0 },
|
|
113
|
+
amount: { type: 'number', default: 0 },
|
|
114
|
+
duration: { type: 'string', default: null, nullable: true },
|
|
115
|
+
source: { type: 'string', default: null, nullable: true },
|
|
116
|
+
resourceId: { type: 'string', default: null, nullable: true },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
roles: {
|
|
120
|
+
$passthrough: true,
|
|
121
|
+
admin: { type: 'boolean', default: false },
|
|
122
|
+
betaTester: { type: 'boolean', default: false },
|
|
123
|
+
developer: { type: 'boolean', default: false },
|
|
124
|
+
},
|
|
125
|
+
flags: {
|
|
126
|
+
$passthrough: true,
|
|
127
|
+
signupProcessed: { type: 'boolean', default: false },
|
|
128
|
+
},
|
|
129
|
+
affiliate: {
|
|
130
|
+
code: { type: 'string', default: '$randomId' },
|
|
131
|
+
referrals: { type: 'array', default: [] },
|
|
132
|
+
},
|
|
133
|
+
metadata: {
|
|
134
|
+
created: '$timestamp:now',
|
|
135
|
+
updated: '$timestamp:now',
|
|
136
|
+
},
|
|
137
|
+
activity: {
|
|
138
|
+
geolocation: {
|
|
139
|
+
ip: { type: 'string', default: null, nullable: true },
|
|
140
|
+
continent: { type: 'string', default: null, nullable: true },
|
|
141
|
+
country: { type: 'string', default: null, nullable: true },
|
|
142
|
+
region: { type: 'string', default: null, nullable: true },
|
|
143
|
+
city: { type: 'string', default: null, nullable: true },
|
|
144
|
+
latitude: { type: 'number', default: 0 },
|
|
145
|
+
longitude: { type: 'number', default: 0 },
|
|
146
|
+
},
|
|
147
|
+
client: {
|
|
148
|
+
language: { type: 'string', default: null, nullable: true },
|
|
149
|
+
mobile: { type: 'boolean', default: false },
|
|
150
|
+
device: { type: 'string', default: null, nullable: true },
|
|
151
|
+
platform: { type: 'string', default: null, nullable: true },
|
|
152
|
+
browser: { type: 'string', default: null, nullable: true },
|
|
153
|
+
vendor: { type: 'string', default: null, nullable: true },
|
|
154
|
+
runtime: { type: 'string', default: null, nullable: true },
|
|
155
|
+
userAgent: { type: 'string', default: null, nullable: true },
|
|
156
|
+
url: { type: 'string', default: null, nullable: true },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
api: {
|
|
160
|
+
clientId: { type: 'string', default: '$uuid' },
|
|
161
|
+
privateKey: { type: 'string', default: '$apiKey' },
|
|
162
|
+
},
|
|
163
|
+
// The documents this account owns, by KIND: `{ teams: ['team-abc'] }`
|
|
164
|
+
// ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)). A counted
|
|
165
|
+
// feature's catalog entry names the kinds its counters mirror onto
|
|
166
|
+
// (`usage: { mirror: ['teams'] }`), and this is what resolves a kind to real
|
|
167
|
+
// document paths — so a mirror is declared once in config instead of
|
|
168
|
+
// re-derived at every call site. Server-written like `usage`: a client that
|
|
169
|
+
// could write it could point another account's counters at its own doc.
|
|
170
|
+
owns: {
|
|
171
|
+
$passthrough: true,
|
|
172
|
+
},
|
|
173
|
+
usage: {
|
|
174
|
+
$passthrough: true,
|
|
175
|
+
// Admin-granted extra credits, feature id → number
|
|
176
|
+
// ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)). Declared
|
|
177
|
+
// HERE so the sibling `$template` never resolves it as a counter block:
|
|
178
|
+
// it is a map of limits, not a map of counts. `usage` is a framework field
|
|
179
|
+
// the security rules deny every client, so an override can only ever be
|
|
180
|
+
// server-written — which is what makes it trustworthy as a limit.
|
|
181
|
+
overrides: { $passthrough: true },
|
|
182
|
+
$template: {
|
|
183
|
+
monthly: { type: 'number', default: 0 },
|
|
184
|
+
daily: { type: 'number', default: 0 },
|
|
185
|
+
total: { type: 'number', default: 0 },
|
|
186
|
+
// When the counter last moved. The old `id` field went with the retired
|
|
187
|
+
// `increment(name, value, { id })` option (#647): nothing writes it now,
|
|
188
|
+
// and a schema field nothing fills reads as a fact that is always null.
|
|
189
|
+
last: {
|
|
190
|
+
timestamp: { type: 'string', default: '$oldDate' },
|
|
191
|
+
timestampUNIX: { type: 'number', default: 0 },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
personal: {
|
|
196
|
+
birthday: '$timestamp',
|
|
197
|
+
gender: { type: 'string', default: null, nullable: true },
|
|
198
|
+
location: {
|
|
199
|
+
country: { type: 'string', default: null, nullable: true },
|
|
200
|
+
region: { type: 'string', default: null, nullable: true },
|
|
201
|
+
city: { type: 'string', default: null, nullable: true },
|
|
202
|
+
postalCode: { type: 'string', default: null, nullable: true },
|
|
203
|
+
street: { type: 'string', default: null, nullable: true },
|
|
204
|
+
},
|
|
205
|
+
name: {
|
|
206
|
+
first: { type: 'string', default: null, nullable: true },
|
|
207
|
+
last: { type: 'string', default: null, nullable: true },
|
|
208
|
+
},
|
|
209
|
+
company: {
|
|
210
|
+
name: { type: 'string', default: null, nullable: true },
|
|
211
|
+
position: { type: 'string', default: null, nullable: true },
|
|
212
|
+
},
|
|
213
|
+
telephone: {
|
|
214
|
+
countryCode: { type: 'number', default: 0 },
|
|
215
|
+
national: { type: 'number', default: 0 },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
connections: {
|
|
219
|
+
$passthrough: true,
|
|
220
|
+
},
|
|
221
|
+
attribution: {
|
|
222
|
+
affiliate: {
|
|
223
|
+
code: { type: 'string', default: null, nullable: true },
|
|
224
|
+
timestamp: { type: 'string', default: null, nullable: true },
|
|
225
|
+
url: { type: 'string', default: null, nullable: true },
|
|
226
|
+
page: { type: 'string', default: null, nullable: true },
|
|
227
|
+
},
|
|
228
|
+
// The touch model: `first` is the visit that first brought the user here,
|
|
229
|
+
// written once and never overwritten; `last` is the newest TAGGED visit. No
|
|
230
|
+
// expiry — the timestamps carry any read-time lookback window. Replaces the
|
|
231
|
+
// single `utm` blob outright (no dual-read).
|
|
232
|
+
first: ATTRIBUTION_TOUCH,
|
|
233
|
+
last: ATTRIBUTION_TOUCH,
|
|
234
|
+
},
|
|
235
|
+
// The tracking-consent snapshot the client captured, stored verbatim: passthrough
|
|
236
|
+
// because this layer never interprets it — the consent module owns its shape. NOT
|
|
237
|
+
// the same thing as `consent` below, which is the legal/marketing decision.
|
|
238
|
+
trackingConsent: {
|
|
239
|
+
$passthrough: true,
|
|
240
|
+
},
|
|
241
|
+
consent: {
|
|
242
|
+
legal: {
|
|
243
|
+
status: { type: 'string', default: 'revoked' },
|
|
244
|
+
grantedAt: {
|
|
245
|
+
timestamp: { type: 'string', default: null, nullable: true },
|
|
246
|
+
timestampUNIX: { type: 'number', default: null, nullable: true },
|
|
247
|
+
source: { type: 'string', default: null, nullable: true },
|
|
248
|
+
ip: { type: 'string', default: null, nullable: true },
|
|
249
|
+
text: { type: 'string', default: null, nullable: true },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
marketing: {
|
|
253
|
+
status: { type: 'string', default: 'revoked' },
|
|
254
|
+
grantedAt: {
|
|
255
|
+
timestamp: { type: 'string', default: null, nullable: true },
|
|
256
|
+
timestampUNIX: { type: 'number', default: null, nullable: true },
|
|
257
|
+
source: { type: 'string', default: null, nullable: true },
|
|
258
|
+
ip: { type: 'string', default: null, nullable: true },
|
|
259
|
+
text: { type: 'string', default: null, nullable: true },
|
|
260
|
+
},
|
|
261
|
+
revokedAt: {
|
|
262
|
+
timestamp: { type: 'string', default: null, nullable: true },
|
|
263
|
+
timestampUNIX: { type: 'number', default: null, nullable: true },
|
|
264
|
+
source: { type: 'string', default: null, nullable: true },
|
|
265
|
+
ip: { type: 'string', default: null, nullable: true },
|
|
266
|
+
text: { type: 'string', default: null, nullable: true },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
module.exports = USER_SCHEMA;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveSubscription — extracted verbatim from @omega.js/backend's User.resolveSubscription
|
|
3
|
+
* (the superset: @omega.js/client's drifted copy lacked `everPaid`).
|
|
4
|
+
*
|
|
5
|
+
* Resolves calculated subscription fields that require derivation logic.
|
|
6
|
+
* Raw data (product.id, status, trial, cancellation) is on the account directly.
|
|
7
|
+
* Returns: { plan, active, trialing, cancelling, everPaid }
|
|
8
|
+
* - plan: the plan ID the user effectively has access to RIGHT NOW ('basic' if cancelled/suspended)
|
|
9
|
+
* - active: user has active access (active, trialing, or cancelling)
|
|
10
|
+
* - trialing: user is in an active trial (status is 'active' but trial hasn't expired)
|
|
11
|
+
* - cancelling: cancellation is pending (status is 'active' but cancellation.pending is true)
|
|
12
|
+
* - everPaid: user has had a paid subscription at some point (payment.startDate exists)
|
|
13
|
+
*/
|
|
14
|
+
function resolveSubscription(account) {
|
|
15
|
+
const subscription = (account?.subscription || account?.properties?.subscription) || {};
|
|
16
|
+
const productId = subscription.product?.id || 'basic';
|
|
17
|
+
|
|
18
|
+
let trialing = false;
|
|
19
|
+
let cancelling = false;
|
|
20
|
+
|
|
21
|
+
if (productId !== 'basic' && subscription.status === 'active') {
|
|
22
|
+
trialing = !!(subscription.trial?.claimed
|
|
23
|
+
&& subscription.trial?.expires?.timestampUNIX > Math.floor(Date.now() / 1000));
|
|
24
|
+
cancelling = !trialing && !!subscription.cancellation?.pending;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const active = (productId !== 'basic' && subscription.status === 'active');
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
plan: active ? productId : 'basic',
|
|
31
|
+
active,
|
|
32
|
+
trialing,
|
|
33
|
+
cancelling,
|
|
34
|
+
everPaid: (subscription.payment?.startDate?.timestampUNIX || 0) > 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = resolveSubscription;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GA4 adapter — Google Analytics 4.
|
|
3
|
+
*
|
|
4
|
+
* The canonical param vocabulary IS GA4's, so every entry without a `map()`
|
|
5
|
+
* passes straight through. Consent category: 'analytics'.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { createAdapter, pick } = require('./resolve.js');
|
|
9
|
+
|
|
10
|
+
// GA4 takes campaign attribution as FLAT event params (the Measurement
|
|
11
|
+
// Protocol's campaign reference), so it rides the payload itself. `gclid` joins
|
|
12
|
+
// them as a plain param: GA4 has no match block to put a click id in, and it is
|
|
13
|
+
// what a Google Ads enhanced-conversions upload will key on when that platform
|
|
14
|
+
// file lands ([#385](https://github.com/Omega-JS-Stack/omega/issues/385)).
|
|
15
|
+
const ATTRIBUTION_PARAMS = ['campaign_id', 'campaign', 'source', 'medium', 'term', 'content', 'gclid'];
|
|
16
|
+
|
|
17
|
+
const adapter = createAdapter({
|
|
18
|
+
provider: 'ga4',
|
|
19
|
+
consentCategory: 'analytics',
|
|
20
|
+
attach: (descriptor, attribution) => {
|
|
21
|
+
Object.assign(descriptor.payload, pick(attribution, ATTRIBUTION_PARAMS));
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
module.exports = adapter;
|
|
26
|
+
module.exports.ATTRIBUTION_PARAMS = ATTRIBUTION_PARAMS;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta adapter — the Facebook Pixel and the Conversions API.
|
|
3
|
+
*
|
|
4
|
+
* Meta's commerce vocabulary (content_ids, num_items) differs from GA4's
|
|
5
|
+
* items[], so those entries carry a `map()` in the catalog; everything else
|
|
6
|
+
* passes through as custom data. Consent category: 'marketing'.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { createAdapter, pick, attachPage } = require('./resolve.js');
|
|
10
|
+
|
|
11
|
+
// Meta's click/browser ids live in the user_data (match) block, never in the
|
|
12
|
+
// event's custom data.
|
|
13
|
+
const ATTRIBUTION_KEYS = ['fbc', 'fbp'];
|
|
14
|
+
|
|
15
|
+
const adapter = createAdapter({
|
|
16
|
+
provider: 'meta',
|
|
17
|
+
consentCategory: 'marketing',
|
|
18
|
+
attach: (descriptor, attribution) => {
|
|
19
|
+
Object.assign(descriptor.userData, pick(attribution, ATTRIBUTION_KEYS));
|
|
20
|
+
// The Conversions API's `event_source_url` (#497) — the transport's to place.
|
|
21
|
+
attachPage(descriptor, attribution);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
module.exports = adapter;
|
|
26
|
+
module.exports.ATTRIBUTION_KEYS = ATTRIBUTION_KEYS;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared adapter mechanism — internal to the adapters.
|
|
3
|
+
*
|
|
4
|
+
* Every adapter is the same three steps: look the canonical event up in the
|
|
5
|
+
* catalog, take THIS provider's mapping (absent → null, the caller logs the
|
|
6
|
+
* dev skip), and run the entry's `map()` (or pass the canonical params
|
|
7
|
+
* through). Only the attribution attachment differs per provider, so that is
|
|
8
|
+
* the one thing an adapter file supplies.
|
|
9
|
+
*
|
|
10
|
+
* Pure: no I/O, no globals, no state. A descriptor is a plain object a
|
|
11
|
+
* transport executes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { entryFor } = require('../catalog.js');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Copy the keys a provider understands out of a flat source object.
|
|
18
|
+
* @param {object} source - The source object (may be undefined).
|
|
19
|
+
* @param {string[]} keys - The keys to take.
|
|
20
|
+
* @returns {object} Only the keys that are present.
|
|
21
|
+
*/
|
|
22
|
+
function pick(source, keys) {
|
|
23
|
+
const out = {};
|
|
24
|
+
if (!source) {
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
if (source[key] !== undefined && source[key] !== null) {
|
|
30
|
+
out[key] = source[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The page a conversion happened on, as the attribution touch captured it. Only
|
|
38
|
+
// the providers whose server API READS one take it (`attachPage` below).
|
|
39
|
+
const PAGE_KEYS = ['url', 'referrer'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attach the conversion's page to a descriptor — the touch's url, and its
|
|
43
|
+
* referrer when there is one
|
|
44
|
+
* ([#497](https://github.com/Omega-JS-Stack/omega/issues/497)).
|
|
45
|
+
*
|
|
46
|
+
* The URL is neither payload nor match data, so it rides its own descriptor
|
|
47
|
+
* member, and only when the touch actually carried one: a server conversion
|
|
48
|
+
* fires from a webhook or an auth trigger, and an INVENTED url is worse to a
|
|
49
|
+
* platform than an absent one. A referrer with no url is not half a page.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} descriptor - The descriptor being built (mutated).
|
|
52
|
+
* @param {object} [attribution] - The flat attribution context.
|
|
53
|
+
*/
|
|
54
|
+
function attachPage(descriptor, attribution) {
|
|
55
|
+
const page = pick(attribution, PAGE_KEYS);
|
|
56
|
+
|
|
57
|
+
if (page.url) {
|
|
58
|
+
descriptor.page = page;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build one provider's adapter.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} options
|
|
66
|
+
* @param {string} options.provider - The provider key, matching the catalog's `providers` key.
|
|
67
|
+
* @param {string} options.consentCategory - 'analytics' | 'marketing'.
|
|
68
|
+
* @param {Function} [options.attach] - (descriptor, attribution) => void; places the
|
|
69
|
+
* captured attribution where THIS provider wants it. Stage C
|
|
70
|
+
* ([#384](https://github.com/Omega-JS-Stack/omega/issues/384)) owns the capture;
|
|
71
|
+
* stage D ([#385](https://github.com/Omega-JS-Stack/omega/issues/385)) fills the
|
|
72
|
+
* rest of `userData` with server match data.
|
|
73
|
+
* @returns {{ provider: string, CONSENT_CATEGORY: string, resolve: Function }}
|
|
74
|
+
*/
|
|
75
|
+
function createAdapter({ provider, consentCategory, attach }) {
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a canonical event into this provider's descriptor.
|
|
78
|
+
* @param {string} canonicalName - The canonical event name.
|
|
79
|
+
* @param {object} [params] - The canonical params.
|
|
80
|
+
* @param {object} [context] - { attribution, consent, runtime }.
|
|
81
|
+
* @returns {{ provider: string, name: string, kind: string, payload: object, userData: object }|null}
|
|
82
|
+
*/
|
|
83
|
+
function resolve(canonicalName, params = {}, context = {}) {
|
|
84
|
+
const entry = entryFor(canonicalName);
|
|
85
|
+
if (!entry) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const mapping = entry.providers[provider];
|
|
90
|
+
if (!mapping) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const descriptor = {
|
|
95
|
+
provider,
|
|
96
|
+
name: mapping.name,
|
|
97
|
+
kind: mapping.kind,
|
|
98
|
+
payload: mapping.map ? mapping.map(params, context) : { ...params },
|
|
99
|
+
// The identity/match block. Empty here by design: attribution lands via
|
|
100
|
+
// `attach` below, and the server enrichment is stage D's.
|
|
101
|
+
userData: {},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// The one mapping shape a transport executes differently: a provider's own
|
|
105
|
+
// pixel method (TikTok's `ttq.page()`), carried only when the catalog names
|
|
106
|
+
// one so nothing has to test for a key that is normally absent.
|
|
107
|
+
if (mapping.method) {
|
|
108
|
+
descriptor.method = mapping.method;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The other optional mapping key a TRANSPORT reads rather than a platform's
|
|
112
|
+
// payload: Meta's `action_source`, the required server-event field naming
|
|
113
|
+
// WHERE a conversion happened. Carried the same way `method` is, only when
|
|
114
|
+
// the catalog overrides the default the sender supplies
|
|
115
|
+
// ([#498](https://github.com/Omega-JS-Stack/omega/issues/498)).
|
|
116
|
+
if (mapping.actionSource) {
|
|
117
|
+
descriptor.actionSource = mapping.actionSource;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (attach) {
|
|
121
|
+
attach(descriptor, context.attribution);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return descriptor;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { provider, CONSENT_CATEGORY: consentCategory, resolve };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { createAdapter, pick, attachPage };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TikTok adapter — the TikTok Pixel and the Events API.
|
|
3
|
+
*
|
|
4
|
+
* TikTok's commerce vocabulary is a `contents` array of product objects
|
|
5
|
+
* (content_id + content_name + price + quantity) beside the order's
|
|
6
|
+
* content_type/value/currency, so those entries carry a `map()` in the catalog;
|
|
7
|
+
* everything else passes through as properties. Consent category: 'marketing'.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { createAdapter, pick, attachPage } = require('./resolve.js');
|
|
11
|
+
|
|
12
|
+
// TikTok's click id and cookie live in the Events API's context.user block —
|
|
13
|
+
// never in the event properties.
|
|
14
|
+
const ATTRIBUTION_KEYS = ['ttclid', 'ttp'];
|
|
15
|
+
|
|
16
|
+
const adapter = createAdapter({
|
|
17
|
+
provider: 'tiktok',
|
|
18
|
+
consentCategory: 'marketing',
|
|
19
|
+
attach: (descriptor, attribution) => {
|
|
20
|
+
Object.assign(descriptor.userData, pick(attribution, ATTRIBUTION_KEYS));
|
|
21
|
+
// The Events API 2.0 data item's `page` member (#497) — the transport's to place.
|
|
22
|
+
attachPage(descriptor, attribution);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
module.exports = adapter;
|
|
27
|
+
module.exports.ATTRIBUTION_KEYS = ATTRIBUTION_KEYS;
|