@tokenoftrust/storefront-runner 2.0.26 → 2.0.28
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/apps/storefront/src/lib/integration/managedInstallDeclarations.ts +5 -3
- package/apps/storefront/src/lib/integration/stageDestinationIds.ts +89 -14
- package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +1 -1
- package/package.json +1 -1
- package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +6 -1
- package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +81 -66
- package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +81 -66
- package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +81 -66
- package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +81 -66
- package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +81 -66
- package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +81 -66
- package/packages/public-runtime/src/gtm-migration/draft-generator.ts +57 -18
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +19 -6
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +49 -15
- package/packages/public-runtime/src/integration/native-amplitude.ts +22 -4
- package/packages/public-runtime/src/integration/native-ga4.ts +69 -38
- package/packages/public-runtime/src/integration/platform-adapters.ts +157 -54
- package/packages/public-runtime/tests/fixtures/integration/managed-analytics-v1/fake-adapter-catalog.json +13 -5
|
@@ -154,76 +154,91 @@
|
|
|
154
154
|
"minProperties": 1,
|
|
155
155
|
"properties": {
|
|
156
156
|
"candidate": {
|
|
157
|
-
"type": "
|
|
158
|
-
"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
157
|
+
"type": "array",
|
|
158
|
+
"minItems": 1,
|
|
159
|
+
"uniqueItems": true,
|
|
160
|
+
"items": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"not": {
|
|
163
|
+
"anyOf": [
|
|
164
|
+
{
|
|
165
|
+
"type": "string",
|
|
166
|
+
"pattern": "(?:[Gg][Tt][Mm]-[A-Za-z0-9]+|[Uu][Aa]-[0-9]+-[0-9]+)"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"type": "string",
|
|
170
|
+
"pattern": "(?:-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----|\\bAKIA[0-9A-Z]{16}\\b|\\bsk-proj-[A-Za-z0-9_-]{8,}\\b|\\bsk-[A-Za-z0-9_-]{20,}\\b|\\b(?:sk|rk)_(?:live|test|prod)_[A-Za-z0-9_-]{8,}\\b|\\b(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]{20,}\\b|\\b(?:npm_|glpat-)[A-Za-z0-9_.-]{8,}\\b|\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b|\\bAIza[A-Za-z0-9_-]{20,}\\b|\\bSG\\.[A-Za-z0-9_.-]{8,}\\b|\\b(?:[Ss]ecret|[Pp]assword|[Cc]redential)[_-][A-Za-z0-9_-]{4,}\\b|\\b[Bb]earer\\s+[A-Za-z0-9._~+/-]{12,}={0,2}\\b|\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]+\\b|\\b(?:api[_ -]?[Kk]ey|client[_ -]?[Ss]ecret|[Pp]assword|[Cc]redential|private[_ -]?[Kk]ey)\\s*[:=]\\s*\\S+)"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"type": "string",
|
|
174
|
+
"pattern": "(?:[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}|\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b|\\b(?:[0-9][ -]?){13,19}\\b|(?:^|[^0-9])(?:\\+?1[ .-]?)?(?:\\([2-9][0-9]{2}\\)|[2-9][0-9]{2})[ .-]?[0-9]{3}[ .-]?[0-9]{4}(?=$|[^0-9]))"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"type": "string",
|
|
178
|
+
"pattern": "(?:</?[A-Za-z][^>]*>|[Jj][Aa][Vv][Aa][Ss][Cc][Rr][Ii][Pp][Tt]\\s*:|[Ee][Vv][Aa][Ll]\\s*\\(|(?:[Aa][Ll][Ee][Rr][Tt]|[Ff][Ee][Tt][Cc][Hh])\\s*\\(|\\{\\{|\\}\\}|\\$\\{|#[A-Za-z_][A-Za-z0-9_-]*|(?:^|[\\s,>+~])\\.[A-Za-z_-][A-Za-z0-9_-]*(?:$|[\\s,>+~.#[\\:])|\\[[A-Za-z_:][-A-Za-z0-9_:.]*(?:[~|^$*]?=[^\\]]+)?\\]|(?:[Ll][Oo][Cc][Aa][Ll]|[Ss][Ee][Ss][Ss][Ii][Oo][Nn])[Ss][Tt][Oo][Rr][Aa][Gg][Ee]|[Ii][Nn][Dd][Ee][Xx][Ee][Dd][Dd][Bb]|[Qq][Uu][Ee][Rr][Yy][Ss][Ee][Ll][Ee][Cc][Tt][Oo][Rr]|(?:[Ww][Ii][Nn][Dd][Oo][Ww]|[Dd][Oo][Cc][Uu][Mm][Ee][Nn][Tt]|[Gg][Ll][Oo][Bb][Aa][Ll][Tt][Hh][Ii][Ss]|[Cc][Oo][Nn][Ss][Oo][Ll][Ee])\\s*(?:\\.|\\[)|[A-Za-z_$][A-Za-z0-9_$]*\\s*\\.\\s*[A-Za-z_$][A-Za-z0-9_$]*\\s*\\(|[A-Za-z_$][A-Za-z0-9_$]*\\s*\\([^)]*\\)\\s*;|[Rr][Ee][Tt][Uu][Rr][Nn][^;{}]*;|[Ff][Uu][Nn][Cc][Tt][Ii][Oo][Nn]|=>|(?:[Vv][Aa][Rr]|[Ll][Ee][Tt]|[Cc][Oo][Nn][Ss][Tt])\\s+[A-Za-z_$])"
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
"minLength": 1
|
|
183
|
+
}
|
|
179
184
|
},
|
|
180
185
|
"staged": {
|
|
181
|
-
"type": "
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
186
|
+
"type": "array",
|
|
187
|
+
"minItems": 1,
|
|
188
|
+
"uniqueItems": true,
|
|
189
|
+
"items": {
|
|
190
|
+
"type": "string",
|
|
191
|
+
"not": {
|
|
192
|
+
"anyOf": [
|
|
193
|
+
{
|
|
194
|
+
"type": "string",
|
|
195
|
+
"pattern": "(?:[Gg][Tt][Mm]-[A-Za-z0-9]+|[Uu][Aa]-[0-9]+-[0-9]+)"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"type": "string",
|
|
199
|
+
"pattern": "(?:-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----|\\bAKIA[0-9A-Z]{16}\\b|\\bsk-proj-[A-Za-z0-9_-]{8,}\\b|\\bsk-[A-Za-z0-9_-]{20,}\\b|\\b(?:sk|rk)_(?:live|test|prod)_[A-Za-z0-9_-]{8,}\\b|\\b(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]{20,}\\b|\\b(?:npm_|glpat-)[A-Za-z0-9_.-]{8,}\\b|\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b|\\bAIza[A-Za-z0-9_-]{20,}\\b|\\bSG\\.[A-Za-z0-9_.-]{8,}\\b|\\b(?:[Ss]ecret|[Pp]assword|[Cc]redential)[_-][A-Za-z0-9_-]{4,}\\b|\\b[Bb]earer\\s+[A-Za-z0-9._~+/-]{12,}={0,2}\\b|\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]+\\b|\\b(?:api[_ -]?[Kk]ey|client[_ -]?[Ss]ecret|[Pp]assword|[Cc]redential|private[_ -]?[Kk]ey)\\s*[:=]\\s*\\S+)"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"type": "string",
|
|
203
|
+
"pattern": "(?:[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}|\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b|\\b(?:[0-9][ -]?){13,19}\\b|(?:^|[^0-9])(?:\\+?1[ .-]?)?(?:\\([2-9][0-9]{2}\\)|[2-9][0-9]{2})[ .-]?[0-9]{3}[ .-]?[0-9]{4}(?=$|[^0-9]))"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"type": "string",
|
|
207
|
+
"pattern": "(?:</?[A-Za-z][^>]*>|[Jj][Aa][Vv][Aa][Ss][Cc][Rr][Ii][Pp][Tt]\\s*:|[Ee][Vv][Aa][Ll]\\s*\\(|(?:[Aa][Ll][Ee][Rr][Tt]|[Ff][Ee][Tt][Cc][Hh])\\s*\\(|\\{\\{|\\}\\}|\\$\\{|#[A-Za-z_][A-Za-z0-9_-]*|(?:^|[\\s,>+~])\\.[A-Za-z_-][A-Za-z0-9_-]*(?:$|[\\s,>+~.#[\\:])|\\[[A-Za-z_:][-A-Za-z0-9_:.]*(?:[~|^$*]?=[^\\]]+)?\\]|(?:[Ll][Oo][Cc][Aa][Ll]|[Ss][Ee][Ss][Ss][Ii][Oo][Nn])[Ss][Tt][Oo][Rr][Aa][Gg][Ee]|[Ii][Nn][Dd][Ee][Xx][Ee][Dd][Dd][Bb]|[Qq][Uu][Ee][Rr][Yy][Ss][Ee][Ll][Ee][Cc][Tt][Oo][Rr]|(?:[Ww][Ii][Nn][Dd][Oo][Ww]|[Dd][Oo][Cc][Uu][Mm][Ee][Nn][Tt]|[Gg][Ll][Oo][Bb][Aa][Ll][Tt][Hh][Ii][Ss]|[Cc][Oo][Nn][Ss][Oo][Ll][Ee])\\s*(?:\\.|\\[)|[A-Za-z_$][A-Za-z0-9_$]*\\s*\\.\\s*[A-Za-z_$][A-Za-z0-9_$]*\\s*\\(|[A-Za-z_$][A-Za-z0-9_$]*\\s*\\([^)]*\\)\\s*;|[Rr][Ee][Tt][Uu][Rr][Nn][^;{}]*;|[Ff][Uu][Nn][Cc][Tt][Ii][Oo][Nn]|=>|(?:[Vv][Aa][Rr]|[Ll][Ee][Tt]|[Cc][Oo][Nn][Ss][Tt])\\s+[A-Za-z_$])"
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
},
|
|
211
|
+
"minLength": 1
|
|
212
|
+
}
|
|
203
213
|
},
|
|
204
214
|
"live": {
|
|
205
|
-
"type": "
|
|
206
|
-
"
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
215
|
+
"type": "array",
|
|
216
|
+
"minItems": 1,
|
|
217
|
+
"uniqueItems": true,
|
|
218
|
+
"items": {
|
|
219
|
+
"type": "string",
|
|
220
|
+
"not": {
|
|
221
|
+
"anyOf": [
|
|
222
|
+
{
|
|
223
|
+
"type": "string",
|
|
224
|
+
"pattern": "(?:[Gg][Tt][Mm]-[A-Za-z0-9]+|[Uu][Aa]-[0-9]+-[0-9]+)"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"type": "string",
|
|
228
|
+
"pattern": "(?:-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----|\\bAKIA[0-9A-Z]{16}\\b|\\bsk-proj-[A-Za-z0-9_-]{8,}\\b|\\bsk-[A-Za-z0-9_-]{20,}\\b|\\b(?:sk|rk)_(?:live|test|prod)_[A-Za-z0-9_-]{8,}\\b|\\b(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]{20,}\\b|\\b(?:npm_|glpat-)[A-Za-z0-9_.-]{8,}\\b|\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b|\\bAIza[A-Za-z0-9_-]{20,}\\b|\\bSG\\.[A-Za-z0-9_.-]{8,}\\b|\\b(?:[Ss]ecret|[Pp]assword|[Cc]redential)[_-][A-Za-z0-9_-]{4,}\\b|\\b[Bb]earer\\s+[A-Za-z0-9._~+/-]{12,}={0,2}\\b|\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]+\\b|\\b(?:api[_ -]?[Kk]ey|client[_ -]?[Ss]ecret|[Pp]assword|[Cc]redential|private[_ -]?[Kk]ey)\\s*[:=]\\s*\\S+)"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"type": "string",
|
|
232
|
+
"pattern": "(?:[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}|\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b|\\b(?:[0-9][ -]?){13,19}\\b|(?:^|[^0-9])(?:\\+?1[ .-]?)?(?:\\([2-9][0-9]{2}\\)|[2-9][0-9]{2})[ .-]?[0-9]{3}[ .-]?[0-9]{4}(?=$|[^0-9]))"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"type": "string",
|
|
236
|
+
"pattern": "(?:</?[A-Za-z][^>]*>|[Jj][Aa][Vv][Aa][Ss][Cc][Rr][Ii][Pp][Tt]\\s*:|[Ee][Vv][Aa][Ll]\\s*\\(|(?:[Aa][Ll][Ee][Rr][Tt]|[Ff][Ee][Tt][Cc][Hh])\\s*\\(|\\{\\{|\\}\\}|\\$\\{|#[A-Za-z_][A-Za-z0-9_-]*|(?:^|[\\s,>+~])\\.[A-Za-z_-][A-Za-z0-9_-]*(?:$|[\\s,>+~.#[\\:])|\\[[A-Za-z_:][-A-Za-z0-9_:.]*(?:[~|^$*]?=[^\\]]+)?\\]|(?:[Ll][Oo][Cc][Aa][Ll]|[Ss][Ee][Ss][Ss][Ii][Oo][Nn])[Ss][Tt][Oo][Rr][Aa][Gg][Ee]|[Ii][Nn][Dd][Ee][Xx][Ee][Dd][Dd][Bb]|[Qq][Uu][Ee][Rr][Yy][Ss][Ee][Ll][Ee][Cc][Tt][Oo][Rr]|(?:[Ww][Ii][Nn][Dd][Oo][Ww]|[Dd][Oo][Cc][Uu][Mm][Ee][Nn][Tt]|[Gg][Ll][Oo][Bb][Aa][Ll][Tt][Hh][Ii][Ss]|[Cc][Oo][Nn][Ss][Oo][Ll][Ee])\\s*(?:\\.|\\[)|[A-Za-z_$][A-Za-z0-9_$]*\\s*\\.\\s*[A-Za-z_$][A-Za-z0-9_$]*\\s*\\(|[A-Za-z_$][A-Za-z0-9_$]*\\s*\\([^)]*\\)\\s*;|[Rr][Ee][Tt][Uu][Rr][Nn][^;{}]*;|[Ff][Uu][Nn][Cc][Tt][Ii][Oo][Nn]|=>|(?:[Vv][Aa][Rr]|[Ll][Ee][Tt]|[Cc][Oo][Nn][Ss][Tt])\\s+[A-Za-z_$])"
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
},
|
|
240
|
+
"minLength": 1
|
|
241
|
+
}
|
|
227
242
|
}
|
|
228
243
|
}
|
|
229
244
|
}
|
|
@@ -60,6 +60,17 @@ export const WS1_GOOGLE_MEASUREMENT_ADAPTER_ID: OpaquePublicId = "platform.googl
|
|
|
60
60
|
*/
|
|
61
61
|
export const WS1_GOOGLE_MEASUREMENT_DESTINATION_KEY = "destinationIds";
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The canonical WS1 catalog's declared cap on the google-measurement adapter's
|
|
65
|
+
* `destinationIds` field (its `maxEntries`) — mirrors
|
|
66
|
+
* `GOOGLE_MEASUREMENT_ADAPTER.maxDestinationsPerStage` in `../integration/
|
|
67
|
+
* platform-adapters.js`. Duplicated rather than imported for the same reason the id
|
|
68
|
+
* and config key above are: this module speaks WS1's public vocabulary only, never a
|
|
69
|
+
* different catalog. Used as the DEFAULT cap when no `adapterCatalog` is supplied;
|
|
70
|
+
* with a catalog, the SELECTED adapter's own declared `maxEntries` wins.
|
|
71
|
+
*/
|
|
72
|
+
const WS1_GOOGLE_MEASUREMENT_DEFAULT_MAX_DESTINATIONS = 4;
|
|
73
|
+
|
|
63
74
|
/** The opaque adapter handle used when a container has no supported GA4 concept. */
|
|
64
75
|
export const NO_ADAPTER_SELECTED_ID: OpaquePublicId = "none-selected";
|
|
65
76
|
|
|
@@ -164,12 +175,22 @@ function selectGoogleMeasurementAdapter(
|
|
|
164
175
|
return catalog?.find((a) => a.adapterId === WS1_GOOGLE_MEASUREMENT_ADAPTER_ID);
|
|
165
176
|
}
|
|
166
177
|
|
|
167
|
-
/** The config field key
|
|
168
|
-
|
|
178
|
+
/** The adapter's declared destination config field: its key, and its entry cap. */
|
|
179
|
+
interface DestinationField {
|
|
180
|
+
readonly key: string;
|
|
181
|
+
readonly maxEntries: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The config field an adapter declares for its Google measurement destination id(s). */
|
|
185
|
+
function destinationFieldFor(adapter: AdapterDefinition): DestinationField {
|
|
169
186
|
const declared =
|
|
170
187
|
adapter.configuration.find((f) => f.key === WS1_GOOGLE_MEASUREMENT_DESTINATION_KEY) ??
|
|
188
|
+
adapter.configuration.find((f) => f.kind === "stageMap") ??
|
|
171
189
|
adapter.configuration.find((f) => f.kind === "string");
|
|
172
|
-
return
|
|
190
|
+
return {
|
|
191
|
+
key: declared?.key ?? WS1_GOOGLE_MEASUREMENT_DESTINATION_KEY,
|
|
192
|
+
maxEntries: declared?.maxEntries ?? WS1_GOOGLE_MEASUREMENT_DEFAULT_MAX_DESTINATIONS,
|
|
193
|
+
};
|
|
173
194
|
}
|
|
174
195
|
|
|
175
196
|
function normalizeType(type: string | null): string {
|
|
@@ -319,41 +340,59 @@ export function generateDisabledMigrationDraft(
|
|
|
319
340
|
const catalogProvided = args.adapterCatalog !== undefined;
|
|
320
341
|
|
|
321
342
|
let proposedAdapterId: OpaquePublicId;
|
|
322
|
-
let
|
|
343
|
+
let destinationField: DestinationField;
|
|
323
344
|
if (!hasGa4 || (catalogProvided && catalogAdapter === undefined)) {
|
|
324
345
|
proposedAdapterId = NO_ADAPTER_SELECTED_ID;
|
|
325
|
-
|
|
346
|
+
destinationField = {
|
|
347
|
+
key: WS1_GOOGLE_MEASUREMENT_DESTINATION_KEY,
|
|
348
|
+
maxEntries: WS1_GOOGLE_MEASUREMENT_DEFAULT_MAX_DESTINATIONS,
|
|
349
|
+
};
|
|
326
350
|
} else if (catalogAdapter !== undefined) {
|
|
327
351
|
proposedAdapterId = catalogAdapter.adapterId;
|
|
328
|
-
|
|
352
|
+
destinationField = destinationFieldFor(catalogAdapter);
|
|
329
353
|
} else {
|
|
330
354
|
proposedAdapterId = WS1_GOOGLE_MEASUREMENT_ADAPTER_ID;
|
|
331
|
-
|
|
355
|
+
destinationField = {
|
|
356
|
+
key: WS1_GOOGLE_MEASUREMENT_DESTINATION_KEY,
|
|
357
|
+
maxEntries: WS1_GOOGLE_MEASUREMENT_DEFAULT_MAX_DESTINATIONS,
|
|
358
|
+
};
|
|
332
359
|
}
|
|
333
360
|
const adapterSelected = proposedAdapterId !== NO_ADAPTER_SELECTED_ID;
|
|
334
361
|
|
|
335
362
|
const proposedConfiguration: Record<string, AdapterConfigValue> = {};
|
|
336
|
-
let
|
|
363
|
+
let destinationIds: string[] = [];
|
|
364
|
+
let destinationIdsCapped = false;
|
|
337
365
|
if (adapterSelected) {
|
|
366
|
+
// Every GA4 config tag on the trigger contributes its literal id — a container can
|
|
367
|
+
// carry a primary property plus a rollup, and each must reach the draft; dropping all
|
|
368
|
+
// but the first silently loses a merchant's destination while the report reads clean.
|
|
369
|
+
const found: string[] = [];
|
|
338
370
|
for (const config of ga4Configs) {
|
|
339
371
|
const raw = nodeByKey.get(config.nodeKey)?.record.raw ?? {};
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
372
|
+
const id = extractDestinationId(raw);
|
|
373
|
+
if (id !== null) found.push(id);
|
|
374
|
+
}
|
|
375
|
+
// Dedupe (the same literal id declared on more than one config tag), preserving
|
|
376
|
+
// deterministic first-seen order.
|
|
377
|
+
destinationIds = [...new Set(found)];
|
|
378
|
+
// A fan-out beyond the adapter's declared cap cannot be represented — refuse the
|
|
379
|
+
// excess rather than guess, and REPORT it (below) rather than drop it silently.
|
|
380
|
+
if (destinationIds.length > destinationField.maxEntries) {
|
|
381
|
+
destinationIdsCapped = true;
|
|
382
|
+
destinationIds = destinationIds.slice(0, destinationField.maxEntries);
|
|
345
383
|
}
|
|
346
|
-
// A container's GA4 id
|
|
347
|
-
// at that stage only. A non-live stage needs its own TEST id, which
|
|
348
|
-
// before verifying — the contract refuses reusing the live one there.
|
|
349
|
-
if (
|
|
384
|
+
// A container's GA4 id(s) are the merchant's LIVE destination(s), so the draft
|
|
385
|
+
// proposes them at that stage only. A non-live stage needs its own TEST id(s), which
|
|
386
|
+
// the merchant adds before verifying — the contract refuses reusing the live one there.
|
|
387
|
+
if (destinationIds.length > 0) proposedConfiguration[destinationField.key] = { live: destinationIds };
|
|
350
388
|
}
|
|
351
389
|
|
|
352
390
|
// --- Redacted finding codes (no source text; stable, sorted-by-construction). -
|
|
353
391
|
const findings: MigrationFindingCode[] = [];
|
|
354
392
|
if (ga4Configs.length > 0) findings.push("ga4-config-detected");
|
|
355
393
|
if (ga4Events.length > 0) findings.push("ga4-events-detected");
|
|
356
|
-
findings.push(
|
|
394
|
+
findings.push(destinationIds.length > 0 ? "ga4-destination-id-configured" : "ga4-destination-id-deferred");
|
|
395
|
+
if (destinationIdsCapped) findings.push("ga4-destination-ids-capped");
|
|
357
396
|
if (!hasGa4) findings.push("no-automatically-portable-analytics");
|
|
358
397
|
if (unresolvedInventory.length > 0) findings.push("unresolved-objects-inventoried");
|
|
359
398
|
|
|
@@ -30,17 +30,29 @@ const REDACTED_PATH = { ...SAFE_CONFIG_STRING, pattern: "^(?:\\$|[A-Za-z][A-Za-z
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* A per-stage destination map — the one non-primitive configuration value, and closed:
|
|
33
|
-
* only the declared stages, only safe strings. `local` is absent
|
|
34
|
-
* loop ships no integrations, so a value there could never fire.
|
|
33
|
+
* only the declared stages, only non-empty arrays of safe strings. `local` is absent
|
|
34
|
+
* because the local dev loop ships no integrations, so a value there could never fire.
|
|
35
|
+
*
|
|
36
|
+
* A stage holds SEVERAL ids because a vendor can natively receive one event at several
|
|
37
|
+
* properties; index 0 is the primary. The per-adapter ceiling is the catalog definition's
|
|
38
|
+
* `maxEntries`, not a number in this schema — this shape only fixes that the values are a
|
|
39
|
+
* closed, ordered, unique list of safe strings.
|
|
35
40
|
*/
|
|
41
|
+
const stageDestinationIds = {
|
|
42
|
+
type: "array",
|
|
43
|
+
minItems: 1,
|
|
44
|
+
uniqueItems: true,
|
|
45
|
+
items: SAFE_NON_EMPTY_STRING,
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
36
48
|
const stageDestinationMap = {
|
|
37
49
|
type: "object",
|
|
38
50
|
additionalProperties: false,
|
|
39
51
|
minProperties: 1,
|
|
40
52
|
properties: {
|
|
41
|
-
candidate:
|
|
42
|
-
staged:
|
|
43
|
-
live:
|
|
53
|
+
candidate: stageDestinationIds,
|
|
54
|
+
staged: stageDestinationIds,
|
|
55
|
+
live: stageDestinationIds,
|
|
44
56
|
},
|
|
45
57
|
} as const;
|
|
46
58
|
|
|
@@ -76,13 +88,14 @@ export const ADAPTER_DEFINITION_V1_JSON_SCHEMA = {
|
|
|
76
88
|
items: {
|
|
77
89
|
type: "object",
|
|
78
90
|
additionalProperties: false,
|
|
79
|
-
required: ["key", "label", "description", "kind", "required"],
|
|
91
|
+
required: ["key", "label", "description", "kind", "required", "maxEntries"],
|
|
80
92
|
properties: {
|
|
81
93
|
key: SAFE_CONFIG_KEY,
|
|
82
94
|
label: SAFE_NON_EMPTY_STRING,
|
|
83
95
|
description: SAFE_NON_EMPTY_STRING,
|
|
84
96
|
kind: { type: "string", enum: ["string", "boolean", "integer", "stageMap"] },
|
|
85
97
|
required: { type: "boolean" },
|
|
98
|
+
maxEntries: { type: "integer", minimum: 1 },
|
|
86
99
|
},
|
|
87
100
|
},
|
|
88
101
|
},
|
|
@@ -43,13 +43,20 @@ export type AdapterConfigStage = (typeof ADAPTER_CONFIG_STAGES)[number];
|
|
|
43
43
|
* A per-stage destination map — the one non-primitive configuration value.
|
|
44
44
|
*
|
|
45
45
|
* Its shape is CLOSED: keys come from {@link ADAPTER_CONFIG_STAGES} and every value is a
|
|
46
|
-
* plain
|
|
47
|
-
* configuration value. So the property that makes this contract safe
|
|
48
|
-
* merchant still cannot supply markup, selectors, secrets, or arbitrary
|
|
49
|
-
* a destination can differ per stage, which is what makes the devbook's
|
|
50
|
-
* non-live stage must not reuse the live property) satisfiable through the
|
|
46
|
+
* non-empty array of plain strings, each held to the same executable-content and PII
|
|
47
|
+
* checks as any other configuration value. So the property that makes this contract safe
|
|
48
|
+
* is intact — a merchant still cannot supply markup, selectors, secrets, or arbitrary
|
|
49
|
+
* structure — while a destination can differ per stage, which is what makes the devbook's
|
|
50
|
+
* own rule (a non-live stage must not reuse the live property) satisfiable through the
|
|
51
|
+
* console at all.
|
|
52
|
+
*
|
|
53
|
+
* A stage takes MULTIPLE ids because a vendor can natively receive one event at several
|
|
54
|
+
* properties (a GA4 primary plus a rollup). Order is significant: index 0 is the primary.
|
|
55
|
+
* How many a stage may hold is the ADAPTER's declared cap ({@link AdapterConfigField}'s
|
|
56
|
+
* `maxEntries`), never a global one — an adapter whose runtime sends to exactly one
|
|
57
|
+
* destination declares 1, so multi-id cannot leak into it silently.
|
|
51
58
|
*/
|
|
52
|
-
export type AdapterConfigStageMap = Partial<Record<AdapterConfigStage, string>>;
|
|
59
|
+
export type AdapterConfigStageMap = Partial<Record<AdapterConfigStage, string[]>>;
|
|
53
60
|
|
|
54
61
|
export type AdapterConfigValue = string | boolean | number | AdapterConfigStageMap;
|
|
55
62
|
/** Public identifiers are opaque handles: never emails, names, or user-provided labels. */
|
|
@@ -65,6 +72,12 @@ export interface AdapterConfigField {
|
|
|
65
72
|
description: string;
|
|
66
73
|
kind: AdapterConfigValueKind;
|
|
67
74
|
required: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* How many values one stage of a `stageMap` may hold — the adapter's own cap, so a
|
|
77
|
+
* console knows whether to offer a second destination without reaching into the
|
|
78
|
+
* runtime. 1 means single-valued. Always 1 for a non-`stageMap` kind.
|
|
79
|
+
*/
|
|
80
|
+
maxEntries: number;
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
export interface AdapterDeprecation {
|
|
@@ -567,20 +580,35 @@ function validateConfig(input: unknown, v: Violations, path: string): void {
|
|
|
567
580
|
for (const [key, value] of Object.entries(obj)) {
|
|
568
581
|
if (!/^[a-z][a-zA-Z0-9]{0,63}$/.test(key)) v.add(pathAt(path, key), "format", "Configuration keys must be lower camel-case identifiers.");
|
|
569
582
|
// A per-stage map is the one permitted non-primitive, and it is CLOSED: stage keys
|
|
570
|
-
// only,
|
|
571
|
-
// no arbitrary structure or depth can enter configuration through it.
|
|
583
|
+
// only, arrays of strings only, each element held to the same executable-content
|
|
584
|
+
// check below. So no arbitrary structure or depth can enter configuration through it.
|
|
572
585
|
if (isStageMap(value)) {
|
|
573
586
|
const entries = Object.entries(value as Record<string, unknown>);
|
|
574
587
|
if (entries.length === 0) v.add(pathAt(path, key), "empty", "A per-stage map must name at least one stage.");
|
|
575
|
-
for (const [stage,
|
|
588
|
+
for (const [stage, ids] of entries) {
|
|
576
589
|
const stagePath = pathAt(pathAt(path, key), stage);
|
|
577
590
|
if (!(ADAPTER_CONFIG_STAGES as readonly string[]).includes(stage)) {
|
|
578
591
|
v.add(stagePath, "unknown_enum", `Stage must be one of: ${ADAPTER_CONFIG_STAGES.join(", ")}.`);
|
|
579
592
|
}
|
|
580
|
-
if (
|
|
581
|
-
v.add(stagePath, "type", "A per-stage value must be
|
|
582
|
-
|
|
583
|
-
|
|
593
|
+
if (!Array.isArray(ids)) {
|
|
594
|
+
v.add(stagePath, "type", "A per-stage value must be an array of destination identifiers.");
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (ids.length === 0) {
|
|
598
|
+
v.add(stagePath, "empty", "A configured stage must name at least one destination identifier.");
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
ids.forEach((id, index) => {
|
|
602
|
+
const idPath = indexAt(stagePath, index);
|
|
603
|
+
if (typeof id !== "string") {
|
|
604
|
+
v.add(idPath, "type", "A destination identifier must be a string.");
|
|
605
|
+
} else if (EXECUTABLE_OR_INSTRUCTION_VALUE.test(id)) {
|
|
606
|
+
v.add(idPath, "executable_value", "Configuration cannot contain markup, JavaScript, templates, selectors, or storage instructions.");
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
const strings = ids.filter((id): id is string => typeof id === "string");
|
|
610
|
+
if (new Set(strings).size !== strings.length) {
|
|
611
|
+
v.add(stagePath, "duplicate", "A stage cannot name the same destination identifier twice.");
|
|
584
612
|
}
|
|
585
613
|
}
|
|
586
614
|
continue;
|
|
@@ -611,10 +639,16 @@ export function validateAdapterDefinition(input: unknown): ContractResult<Adapte
|
|
|
611
639
|
if (!Array.isArray(fields)) v.add("configuration", "type", "Expected an array.");
|
|
612
640
|
else fields.forEach((field, i) => {
|
|
613
641
|
const path = `configuration[${i}]`; const f = object(field, v, path); if (!f) return;
|
|
614
|
-
rejectKeys(f, ["key", "label", "description", "kind", "required"], v, path);
|
|
642
|
+
rejectKeys(f, ["key", "label", "description", "kind", "required", "maxEntries"], v, path);
|
|
615
643
|
const key = string(f, "key", v, path); if (key && (!/^[a-z][a-zA-Z0-9]{0,63}$/.test(key) || isForbiddenKeyName(key))) v.add(`${path}.key`, "forbidden_field", "Configuration field names must be safe lower camel-case identifiers.");
|
|
616
|
-
string(f, "label", v, path); string(f, "description", v, path); enumeration(f, "kind", ADAPTER_CONFIG_VALUE_KINDS, v, path);
|
|
644
|
+
string(f, "label", v, path); string(f, "description", v, path); const kind = enumeration(f, "kind", ADAPTER_CONFIG_VALUE_KINDS, v, path);
|
|
617
645
|
if (typeof f.required !== "boolean") v.add(`${path}.required`, "type", "Expected a boolean.");
|
|
646
|
+
const maxEntries = integer(f, "maxEntries", v, path, 1);
|
|
647
|
+
// Only a stageMap holds several values, so any other kind declaring a cap above 1
|
|
648
|
+
// would advertise a fan-out its runtime has no way to perform.
|
|
649
|
+
if (kind !== undefined && kind !== "stageMap" && maxEntries !== undefined && maxEntries !== 1) {
|
|
650
|
+
v.add(`${path}.maxEntries`, "state", "Only a stageMap field may declare more than one entry.");
|
|
651
|
+
}
|
|
618
652
|
});
|
|
619
653
|
if (Array.isArray(fields)) {
|
|
620
654
|
const keys = fields.flatMap((field) => isPlainObject(field) && typeof field.key === "string" ? [field.key] : []);
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
validateCommerceEvent,
|
|
15
15
|
type CommerceEvent,
|
|
16
16
|
} from "./events.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
validateBrowserEnvironment,
|
|
19
|
+
type BrowserEnvironment,
|
|
20
|
+
type IntegrationEnvironment,
|
|
21
|
+
} from "./environment.js";
|
|
18
22
|
import { resemblesManagedAnalyticsPii } from "./managed-analytics-value-safety.js";
|
|
19
23
|
import type {
|
|
20
24
|
IntegrationEventSubscribe,
|
|
@@ -24,7 +28,7 @@ import type {
|
|
|
24
28
|
import {
|
|
25
29
|
AMPLITUDE_HTTP_ADAPTER,
|
|
26
30
|
destinationIdConfigKey,
|
|
27
|
-
|
|
31
|
+
resolveAdapterDestinationIds,
|
|
28
32
|
} from "./platform-adapters.js";
|
|
29
33
|
|
|
30
34
|
export const AMPLITUDE_COLLECTION_ORIGIN = "https://api2.amplitude.com" as const;
|
|
@@ -49,6 +53,20 @@ export interface AmplitudeHttpEvent {
|
|
|
49
53
|
export const AMPLITUDE_DEDUPE_CAPACITY = 512 as const;
|
|
50
54
|
const MAX_AMPLITUDE_DEDUPE_CAPACITY = 2_048;
|
|
51
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Amplitude is single-valued: the reviewed HTTP body carries one `api_key`, so this
|
|
58
|
+
* adapter has nowhere to put a second project key. A stage that somehow names more than
|
|
59
|
+
* one resolves to nothing rather than to whichever id came first — collecting to an
|
|
60
|
+
* arbitrary subset of what a merchant configured is the failure this refuses.
|
|
61
|
+
*/
|
|
62
|
+
function soleDestinationId(
|
|
63
|
+
publicConfig: Record<string, unknown>,
|
|
64
|
+
environment: IntegrationEnvironment,
|
|
65
|
+
): string | null {
|
|
66
|
+
const ids = resolveAdapterDestinationIds(AMPLITUDE_HTTP_ADAPTER, publicConfig, environment);
|
|
67
|
+
return ids.length === 1 ? ids[0] as string : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
export type AmplitudeDiagnosticCode =
|
|
53
71
|
| "collection_failed"
|
|
54
72
|
| "subscription_bind_failed"
|
|
@@ -201,7 +219,7 @@ export function registerAmplitudeHttpDestination(
|
|
|
201
219
|
try {
|
|
202
220
|
environment = validateBrowserEnvironment(input.environment);
|
|
203
221
|
destinationId = environment.ok
|
|
204
|
-
?
|
|
222
|
+
? soleDestinationId(input.publicConfig, environment.value.environment)
|
|
205
223
|
: null;
|
|
206
224
|
fetcher = input.fetch;
|
|
207
225
|
observer = input.onDiagnostic;
|
|
@@ -297,7 +315,7 @@ export function createAmplitudeLifecycleModule(
|
|
|
297
315
|
ctx.consent.eligiblePurposes.includes("analytics");
|
|
298
316
|
subscribe = ctx.subscribe;
|
|
299
317
|
destinationId = environment.ok
|
|
300
|
-
?
|
|
318
|
+
? soleDestinationId(ctx.config, environment.value.environment)
|
|
301
319
|
: null;
|
|
302
320
|
if (
|
|
303
321
|
ctx.appId !== AMPLITUDE_HTTP_ADAPTER.adapterId || !eligible || !subscribe ||
|