@tokenoftrust/storefront-runner 2.0.26 → 2.0.27

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 (17) hide show
  1. package/apps/storefront/src/lib/integration/stageDestinationIds.ts +67 -14
  2. package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +1 -1
  3. package/package.json +1 -1
  4. package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +6 -1
  5. package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +81 -66
  6. package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +81 -66
  7. package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +81 -66
  8. package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +81 -66
  9. package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +81 -66
  10. package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +81 -66
  11. package/packages/public-runtime/src/gtm-migration/draft-generator.ts +1 -1
  12. package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +19 -6
  13. package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +49 -15
  14. package/packages/public-runtime/src/integration/native-amplitude.ts +22 -4
  15. package/packages/public-runtime/src/integration/native-ga4.ts +69 -38
  16. package/packages/public-runtime/src/integration/platform-adapters.ts +157 -54
  17. 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": "string",
158
- "not": {
159
- "anyOf": [
160
- {
161
- "type": "string",
162
- "pattern": "(?:[Gg][Tt][Mm]-[A-Za-z0-9]+|[Uu][Aa]-[0-9]+-[0-9]+)"
163
- },
164
- {
165
- "type": "string",
166
- "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+)"
167
- },
168
- {
169
- "type": "string",
170
- "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]))"
171
- },
172
- {
173
- "type": "string",
174
- "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_$])"
175
- }
176
- ]
177
- },
178
- "minLength": 1
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": "string",
182
- "not": {
183
- "anyOf": [
184
- {
185
- "type": "string",
186
- "pattern": "(?:[Gg][Tt][Mm]-[A-Za-z0-9]+|[Uu][Aa]-[0-9]+-[0-9]+)"
187
- },
188
- {
189
- "type": "string",
190
- "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+)"
191
- },
192
- {
193
- "type": "string",
194
- "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]))"
195
- },
196
- {
197
- "type": "string",
198
- "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_$])"
199
- }
200
- ]
201
- },
202
- "minLength": 1
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": "string",
206
- "not": {
207
- "anyOf": [
208
- {
209
- "type": "string",
210
- "pattern": "(?:[Gg][Tt][Mm]-[A-Za-z0-9]+|[Uu][Aa]-[0-9]+-[0-9]+)"
211
- },
212
- {
213
- "type": "string",
214
- "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+)"
215
- },
216
- {
217
- "type": "string",
218
- "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]))"
219
- },
220
- {
221
- "type": "string",
222
- "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_$])"
223
- }
224
- ]
225
- },
226
- "minLength": 1
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
  }
@@ -346,7 +346,7 @@ export function generateDisabledMigrationDraft(
346
346
  // A container's GA4 id is the merchant's LIVE destination, so the draft proposes it
347
347
  // at that stage only. A non-live stage needs its own TEST id, which the merchant adds
348
348
  // before verifying — the contract refuses reusing the live one there.
349
- if (destinationId !== null) proposedConfiguration[destinationKey] = { live: destinationId };
349
+ if (destinationId !== null) proposedConfiguration[destinationKey] = { live: [destinationId] };
350
350
  }
351
351
 
352
352
  // --- Redacted finding codes (no source text; stable, sorted-by-construction). -
@@ -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 because the local dev
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: SAFE_NON_EMPTY_STRING,
42
- staged: SAFE_NON_EMPTY_STRING,
43
- live: SAFE_NON_EMPTY_STRING,
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 string held to the same executable-content and PII checks as any other
47
- * configuration value. So the property that makes this contract safe is intact — a
48
- * merchant still cannot supply markup, selectors, secrets, or arbitrary structure — while
49
- * a destination can differ per stage, which is what makes the devbook's own rule (a
50
- * non-live stage must not reuse the live property) satisfiable through the console at all.
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, string values only, each held to the same executable-content check below. So
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, id] of entries) {
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 (typeof id !== "string") {
581
- v.add(stagePath, "type", "A per-stage value must be a string.");
582
- } else if (EXECUTABLE_OR_INSTRUCTION_VALUE.test(id)) {
583
- v.add(stagePath, "executable_value", "Configuration cannot contain markup, JavaScript, templates, selectors, or storage instructions.");
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 { validateBrowserEnvironment, type BrowserEnvironment } from "./environment.js";
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
- resolveAdapterDestinationId,
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
- ? resolveAdapterDestinationId(input.publicConfig, environment.value.environment)
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
- ? resolveAdapterDestinationId(ctx.config, environment.value.environment)
318
+ ? soleDestinationId(ctx.config, environment.value.environment)
301
319
  : null;
302
320
  if (
303
321
  ctx.appId !== AMPLITUDE_HTTP_ADAPTER.adapterId || !eligible || !subscribe ||
@@ -6,12 +6,21 @@
6
6
  * or persist browser identity. A reviewed direct POST sends a closed, anonymous
7
7
  * payload to one exact collection origin after the shared broker has applied consent
8
8
  * and logical-event dedupe.
9
+ *
10
+ * A stage may configure SEVERAL measurement ids (a primary plus a rollup). GA4 has no
11
+ * multi-property hit — `tid` names exactly one measurement id — so one logical event
12
+ * becomes one POST per id, identical but for `tid`. The fan-out sits below the broker's
13
+ * dedupe, so a logical action is still measured exactly once per property.
9
14
  */
10
15
  import type { IntegrationLifecycleContext, IntegrationLifecycleModule, LifecycleUpdateReason } from "./lifecycle.js";
11
16
  import type { CommerceEvent, CommerceEventType } from "./events.js";
12
17
  import { APPROVED_MARKETING_ACTION_NAMES, validateCommerceEvent } from "./events.js";
13
18
  import { isPurposeEligible, parseConsentSnapshot } from "./consent-broker.js";
14
- import { destinationIdConfigKey, GOOGLE_MEASUREMENT_ADAPTER } from "./platform-adapters.js";
19
+ import {
20
+ destinationIdConfigKey,
21
+ GOOGLE_MEASUREMENT_ADAPTER,
22
+ resolveAdapterDestinationIds,
23
+ } from "./platform-adapters.js";
15
24
  import { INTEGRATION_ENVIRONMENTS, type IntegrationEnvironment } from "./environment.js";
16
25
  import { resemblesManagedAnalyticsPii } from "./managed-analytics-value-safety.js";
17
26
 
@@ -63,7 +72,8 @@ export interface NativeGa4ModuleOptions {
63
72
  }
64
73
 
65
74
  interface ActiveGa4Config {
66
- destinationId: string;
75
+ /** Every measurement id this stage collects to, in configured order. */
76
+ destinationIds: readonly string[];
67
77
  environment: IntegrationEnvironment;
68
78
  siteSurface: IntegrationLifecycleContext["environment"]["siteSurface"];
69
79
  }
@@ -84,24 +94,28 @@ function configuredFor(ctx: IntegrationLifecycleContext): ActiveGa4Config | null
84
94
  if (!CONFIG_KEYS.has(key)) return null;
85
95
  }
86
96
 
87
- const liveId = ctx.config[destinationIdConfigKey("live")];
88
- if (typeof liveId !== "string" || !GOOGLE_MEASUREMENT_ADAPTER.validateId(liveId)) return null;
97
+ // Every stage present must decode to a valid, in-cap set, and no non-live stage may
98
+ // reuse ANY live id — checked across the whole config, not just the active stage, so a
99
+ // misconfigured neighbour resolves the entire integration to nothing rather than
100
+ // collecting from a page whose other stages would leak.
101
+ const liveIds = resolveAdapterDestinationIds(GOOGLE_MEASUREMENT_ADAPTER, ctx.config, "live");
102
+ if (liveIds.length === 0) return null;
103
+ const live = new Set(liveIds);
89
104
  for (const environment of INTEGRATION_ENVIRONMENTS) {
90
- const value = ctx.config[destinationIdConfigKey(environment)];
91
- if (value === undefined) continue;
92
- if (typeof value !== "string" || !GOOGLE_MEASUREMENT_ADAPTER.validateId(value)) return null;
93
- if (environment !== "live" && value === liveId) return null;
105
+ if (ctx.config[destinationIdConfigKey(environment)] === undefined) continue;
106
+ const ids = resolveAdapterDestinationIds(GOOGLE_MEASUREMENT_ADAPTER, ctx.config, environment);
107
+ if (ids.length === 0) return null;
108
+ if (environment !== "live" && ids.some((id) => live.has(id))) return null;
94
109
  }
95
110
 
96
- const destinationId = ctx.config[destinationIdConfigKey(ctx.environment.environment)];
97
- if (
98
- typeof destinationId !== "string" ||
99
- !GOOGLE_MEASUREMENT_ADAPTER.validateId(destinationId)
100
- ) {
101
- return null;
102
- }
111
+ const destinationIds = resolveAdapterDestinationIds(
112
+ GOOGLE_MEASUREMENT_ADAPTER,
113
+ ctx.config,
114
+ ctx.environment.environment,
115
+ );
116
+ if (destinationIds.length === 0) return null;
103
117
  return {
104
- destinationId,
118
+ destinationIds,
105
119
  environment: ctx.environment.environment,
106
120
  siteSurface: ctx.environment.siteSurface,
107
121
  };
@@ -134,28 +148,37 @@ function exactKeys(value: Record<string, unknown>, allowed: readonly string[]):
134
148
  return actual.length <= allowed.length && actual.every((key) => allowed.includes(key));
135
149
  }
136
150
 
137
- function mapEvent(config: ActiveGa4Config, input: unknown): NativeGa4CollectRequest | null {
151
+ /**
152
+ * Build the collect requests one event becomes — ONE per configured measurement id.
153
+ *
154
+ * GA4 has no multi-property hit: `tid` names exactly one measurement id, so reaching a
155
+ * primary and a rollup means repeating the same closed payload with a different `tid`.
156
+ * Returns an empty list when the event does not map, so a caller cannot mistake "not
157
+ * measured" for "measured once".
158
+ */
159
+ function mapEvent(config: ActiveGa4Config, input: unknown): NativeGa4CollectRequest[] {
138
160
  const validated = validateCommerceEvent(input);
139
- if (!validated.ok) return null;
161
+ if (!validated.ok) return [];
140
162
  const event = validated.value;
141
- if (!NATIVE_GA4_EVENT_SET.has(event.type)) return null;
142
- if (event.storefront.emitter !== "browser") return null;
143
- if (event.storefront.environment !== config.environment) return null;
144
- if (event.storefront.siteSurface !== config.siteSurface) return null;
145
- if (!event.consent.eligiblePurposes.includes("analytics")) return null;
146
-
163
+ if (!NATIVE_GA4_EVENT_SET.has(event.type)) return [];
164
+ if (event.storefront.emitter !== "browser") return [];
165
+ if (event.storefront.environment !== config.environment) return [];
166
+ if (event.storefront.siteSurface !== config.siteSurface) return [];
167
+ if (!event.consent.eligiblePurposes.includes("analytics")) return [];
168
+
169
+ // `tid` is the one field that differs between the fan-out requests; it is added per
170
+ // destination below.
147
171
  const fields: Record<string, string> = {
148
172
  v: "2",
149
- tid: config.destinationId,
150
173
  cid: anonymousClientId(event),
151
174
  };
152
175
 
153
176
  if (event.type === "storefront.page.viewed.v1") {
154
- if (!exactKeys(event.data, ["path"]) || typeof event.data["path"] !== "string") return null;
177
+ if (!exactKeys(event.data, ["path"]) || typeof event.data["path"] !== "string") return [];
155
178
  fields["en"] = "page_view";
156
179
  fields["ep.page_path"] = event.data["path"];
157
180
  } else if (event.type === "storefront.page.viewed.v2") {
158
- if (!exactKeys(event.data, ["path", "attribution"]) || typeof event.data["path"] !== "string") return null;
181
+ if (!exactKeys(event.data, ["path", "attribution"]) || typeof event.data["path"] !== "string") return [];
159
182
  fields["en"] = "page_view";
160
183
  fields["ep.page_path"] = event.data["path"];
161
184
  const attribution = event.data["attribution"];
@@ -175,13 +198,16 @@ function mapEvent(config: ActiveGa4Config, input: unknown): NativeGa4CollectRequ
175
198
  }
176
199
  }
177
200
  } else {
178
- if (!exactKeys(event.data, ["name"])) return null;
201
+ if (!exactKeys(event.data, ["name"])) return [];
179
202
  const name = event.data["name"];
180
- if (typeof name !== "string" || !APPROVED_ACTION_SET.has(name)) return null;
203
+ if (typeof name !== "string" || !APPROVED_ACTION_SET.has(name)) return [];
181
204
  fields["en"] = name;
182
205
  }
183
206
 
184
- return Object.freeze({ endpoint: GA4_COLLECTION_ENDPOINT, fields: Object.freeze(fields) });
207
+ return config.destinationIds.map((tid) => Object.freeze({
208
+ endpoint: GA4_COLLECTION_ENDPOINT,
209
+ fields: Object.freeze({ ...fields, tid }),
210
+ }));
185
211
  }
186
212
 
187
213
  /**
@@ -237,8 +263,8 @@ export function createNativeGa4Module(options: NativeGa4ModuleOptions): Integrat
237
263
  observeSafely(options.observe, "duplicate_suppressed");
238
264
  return;
239
265
  }
240
- const request = mapEvent(current, event);
241
- if (!request) {
266
+ const requests = mapEvent(current, event);
267
+ if (requests.length === 0) {
242
268
  observeSafely(options.observe, "event_rejected");
243
269
  return;
244
270
  }
@@ -248,13 +274,18 @@ export function createNativeGa4Module(options: NativeGa4ModuleOptions): Integrat
248
274
  const oldest = deliveredOrder.shift();
249
275
  if (oldest !== undefined) delivered.delete(oldest);
250
276
  }
251
- try {
252
- const result = options.transport(request);
253
- if (result && typeof result.then === "function") {
254
- void result.catch(() => observeSafely(options.observe, "transport_failed"));
277
+ // One logical event, one send per configured measurement id. Each send is
278
+ // independent: a destination whose transport fails is observed on its own and
279
+ // never suppresses the others.
280
+ for (const request of requests) {
281
+ try {
282
+ const result = options.transport(request);
283
+ if (result && typeof result.then === "function") {
284
+ void result.catch(() => observeSafely(options.observe, "transport_failed"));
285
+ }
286
+ } catch {
287
+ observeSafely(options.observe, "transport_failed");
255
288
  }
256
- } catch {
257
- observeSafely(options.observe, "transport_failed");
258
289
  }
259
290
  },
260
291
  },