@tokenoftrust/storefront-runner 2.0.27 → 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.
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "@tot/public-runtime";
|
|
13
13
|
import { AppVersionStore } from "../apps/appVersionStore.js";
|
|
14
14
|
import type { Queryable } from "../d1/catalog.js";
|
|
15
|
-
import { parseStageDestinationIds } from "./stageDestinationIds.js";
|
|
15
|
+
import { migrateLegacyDestinationIds, parseStageDestinationIds } from "./stageDestinationIds.js";
|
|
16
16
|
|
|
17
17
|
const MANAGED_KIND = "managed-analytics-v1";
|
|
18
18
|
|
|
@@ -51,9 +51,11 @@ export async function loadActiveManagedInstallDeclarations(
|
|
|
51
51
|
if (current.facets.digest !== manifest.value.app.digest || manifest.value.app.id !== row.app_id) continue;
|
|
52
52
|
// The stored per-stage map, re-parsed through the SAME validator the API used, so a
|
|
53
53
|
// row that no longer satisfies the contract disables its install rather than
|
|
54
|
-
// resolving a partial or stale destination set.
|
|
54
|
+
// resolving a partial or stale destination set. A row saved before per-stage
|
|
55
|
+
// arrays existed is lifted into the current shape first, so an install predating
|
|
56
|
+
// that change keeps firing instead of silently going inert underneath its tenant.
|
|
55
57
|
const ids = parseStageDestinationIds(
|
|
56
|
-
current.facets.publicConfig
|
|
58
|
+
migrateLegacyDestinationIds(current.facets.publicConfig),
|
|
57
59
|
getPlatformAdapter(row.app_id),
|
|
58
60
|
);
|
|
59
61
|
if (!ids.ok) continue;
|
|
@@ -154,3 +154,25 @@ export function parseStageDestinationIds(
|
|
|
154
154
|
if (details.length > 0) return { ok: false, details };
|
|
155
155
|
return { ok: true, value };
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Lift a stored install's public config into the shape {@link parseStageDestinationIds}
|
|
160
|
+
* expects, tolerating the two shapes that predate the per-stage array contract: the
|
|
161
|
+
* original single `destinationId` (implicitly `live`, from before per-stage support
|
|
162
|
+
* existed at all) and the per-stage map whose values were bare strings rather than
|
|
163
|
+
* arrays (the shape between per-stage support landing and it going array-valued).
|
|
164
|
+
* Anything already array-shaped passes through untouched, so this is idempotent and
|
|
165
|
+
* safe to run on every read — never on a submission, which always arrives in the
|
|
166
|
+
* current shape already.
|
|
167
|
+
*/
|
|
168
|
+
export function migrateLegacyDestinationIds(publicConfig: Record<string, unknown>): unknown {
|
|
169
|
+
const staged = publicConfig["destinationIds"];
|
|
170
|
+
if (staged !== undefined) {
|
|
171
|
+
if (typeof staged !== "object" || staged === null || Array.isArray(staged)) return staged;
|
|
172
|
+
const entries = Object.entries(staged as Record<string, unknown>);
|
|
173
|
+
if (entries.every(([, ids]) => Array.isArray(ids))) return staged;
|
|
174
|
+
return Object.fromEntries(entries.map(([stage, ids]) => [stage, Array.isArray(ids) ? ids : [ids]]));
|
|
175
|
+
}
|
|
176
|
+
const legacy = publicConfig["destinationId"];
|
|
177
|
+
return typeof legacy === "string" && legacy.length > 0 ? { live: [legacy] } : staged;
|
|
178
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.28",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|
|
@@ -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
|
|