@getuserfeedback/adapters 0.4.4 → 0.5.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.
@@ -1,31 +1,37 @@
1
- import { DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS, } from "@getuserfeedback/protocol";
2
- import { DEFAULT_GTM_SCOPE_MAPPINGS, ESSENTIAL_GTM_SCOPES, GTM_CONSENT_TYPES, GTM_TEMPLATE_MAPPING_OPTIONS, GTM_TEMPLATE_SCOPE_OPTIONS, isHostConfigurableGtmScope, } from "./core.js";
1
+ import { DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS } from "@getuserfeedback/protocol";
2
+ import { GTM_CONSENT_TYPES } from "./core.js";
3
+ import { BUILD_GTM_IDENTIFY_COMMAND_SOURCE, GTM_QUEUE_CLIENT_META, RESOLVE_GTM_THEME_INPUT_FROM_TEMPLATE_DATA_SOURCE, } from "./runtime-model.js";
3
4
  export const DEFAULT_GTM_LOADER_SCRIPT_BASE_URL = "https://cdn.getuserfeedback.com/widget/loader/v1";
4
5
  const GTM_TEMPLATE_VERSION = 1;
5
6
  const GTM_TEMPLATE_DEFAULT_DISPLAY_NAME = "getuserfeedback Widget";
6
- const GTM_TEMPLATE_DEFAULT_DESCRIPTION = "Load the getuserfeedback widget with Google Tag Manager, including theme settings and consent defaults.";
7
+ const GTM_TEMPLATE_DEFAULT_DESCRIPTION = "Load the getuserfeedback widget with Google Tag Manager, including color scheme settings and user identity.";
8
+ const GTM_TEMPLATE_DEFAULT_ID = "cvt_temp_public_id";
9
+ const GTM_TEMPLATE_GALLERY_DISPLAY_NAME = "getuserfeedback.com";
10
+ const GTM_TEMPLATE_GALLERY_DESCRIPTION = "In-app user onboarding, surveys, and embedded forms for feedback-fueled product growth";
11
+ const GTM_TEMPLATE_DEFAULT_BRAND_DISPLAY_NAME = "getuserfeedback.com";
12
+ const GTM_TEMPLATE_DEFAULT_BRAND_THUMBNAIL = "data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=";
13
+ const GTM_TEMPLATE_GALLERY_CATEGORIES = [
14
+ "CONVERSIONS",
15
+ "ANALYTICS",
16
+ "SURVEY",
17
+ ];
7
18
  const GTM_TEMPLATE_CATEGORIES = [
8
19
  "SURVEY",
9
20
  "PERSONALIZATION",
10
21
  "UTILITY",
11
22
  ];
12
23
  const GTM_TEMPLATE_CONTAINER_CONTEXTS = ["WEB"];
24
+ const GTM_TEMPLATE_GALLERY_TERMS_OF_SERVICE = [
25
+ "By creating or modifying this file you agree to Google Tag Manager's Community",
26
+ "Template Gallery Developer Terms of Service available at",
27
+ "https://developers.google.com/tag-manager/gallery-tos (or such other URL as",
28
+ "Google may provide), as modified from time to time.",
29
+ ].join("\n");
30
+ const GTM_TEMPLATE_DEFAULT_NOTES = "Generated from @getuserfeedback/adapters/gtm.";
31
+ const GTM_TEMPLATE_DEFAULT_TESTS = "scenarios: []";
32
+ const GTM_TEMPLATE_UTF8_BOM = "\uFEFF";
13
33
  const GTM_QUEUE_KEY = "__getuserfeedback_queue";
14
34
  const NON_EMPTY_VALIDATOR = { type: "NON_EMPTY" };
15
- const MANUAL_CONSENT_MODE_CONDITION = [
16
- {
17
- paramName: "consentMode",
18
- paramValue: "manual",
19
- type: "EQUALS",
20
- },
21
- ];
22
- const AUTOMATIC_CONSENT_MODE_CONDITION = [
23
- {
24
- paramName: "consentMode",
25
- paramValue: "automatic",
26
- type: "EQUALS",
27
- },
28
- ];
29
35
  const IDENTIFY_ADVANCED_MODE_CONDITION = [
30
36
  {
31
37
  paramName: "identifyMode",
@@ -49,6 +55,42 @@ const INTELLIGENT_IDENTIFY_EMAIL_KEYS = [
49
55
  "context.email",
50
56
  "context.user.email",
51
57
  ];
58
+ const INTELLIGENT_IDENTIFY_FIRST_NAME_KEYS = [
59
+ "firstName",
60
+ "first_name",
61
+ "user.firstName",
62
+ "user.first_name",
63
+ "user.givenName",
64
+ "user.given_name",
65
+ "traits.firstName",
66
+ "traits.first_name",
67
+ "traits.givenName",
68
+ "traits.given_name",
69
+ "context.firstName",
70
+ "context.first_name",
71
+ "context.user.firstName",
72
+ "context.user.first_name",
73
+ "context.user.givenName",
74
+ "context.user.given_name",
75
+ ];
76
+ const INTELLIGENT_IDENTIFY_LAST_NAME_KEYS = [
77
+ "lastName",
78
+ "last_name",
79
+ "user.lastName",
80
+ "user.last_name",
81
+ "user.familyName",
82
+ "user.family_name",
83
+ "traits.lastName",
84
+ "traits.last_name",
85
+ "traits.familyName",
86
+ "traits.family_name",
87
+ "context.lastName",
88
+ "context.last_name",
89
+ "context.user.lastName",
90
+ "context.user.last_name",
91
+ "context.user.familyName",
92
+ "context.user.family_name",
93
+ ];
52
94
  const INTELLIGENT_IDENTIFY_PHONE_KEYS = [
53
95
  "phone",
54
96
  "user.phone",
@@ -70,31 +112,6 @@ const ADVANCED_PRIMARY_IDENTITY_SELECT_ITEMS = [
70
112
  value: "email",
71
113
  },
72
114
  ];
73
- const buildAutomaticDefaultScopeMappings = () => DEFAULT_GTM_SCOPE_MAPPINGS.filter((mapping) => isHostConfigurableGtmScope(mapping.scope)).map((mapping) => ({
74
- scope: mapping.scope,
75
- mapping: mapping.consentType,
76
- }));
77
- const buildHostConfigurableDefaultScopeMappings = () => [
78
- ...buildAutomaticDefaultScopeMappings(),
79
- {
80
- scope: "analytics.measurement",
81
- mapping: "analytics_storage",
82
- },
83
- ];
84
- const buildScopeParameterHelp = () => [
85
- "Some required widget storage settings are always enabled and do not appear here.",
86
- `${ESSENTIAL_GTM_SCOPES[0]} and ${ESSENTIAL_GTM_SCOPES[1]} are always enabled so the widget can load and keep itself secure.`,
87
- "",
88
- ...GTM_TEMPLATE_SCOPE_OPTIONS.filter((option) => isHostConfigurableGtmScope(option.scope)).map((option) => `${option.label}: ${option.description}`),
89
- ].join("\n");
90
- const buildHostConfigurableScopeSelectItems = () => GTM_TEMPLATE_SCOPE_OPTIONS.filter((option) => isHostConfigurableGtmScope(option.scope)).map((option) => ({
91
- value: option.scope,
92
- displayValue: option.label,
93
- }));
94
- const buildGtmConsentTypeSelectItems = () => GTM_TEMPLATE_MAPPING_OPTIONS.map((option) => ({
95
- value: option.consentType,
96
- displayValue: `${option.label} Granted`,
97
- }));
98
115
  const trimTrailingSlash = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
99
116
  const stringValue = (value) => ({
100
117
  type: 1,
@@ -134,29 +151,61 @@ const buildPermission = (publicId, params) => ({
134
151
  },
135
152
  isRequired: true,
136
153
  });
154
+ const normalizeAdditionalLoaderScriptUrlPatterns = (patterns) => {
155
+ if (!patterns) {
156
+ return [];
157
+ }
158
+ const normalizedPatterns = new Set();
159
+ for (const pattern of patterns) {
160
+ const normalizedPattern = pattern.trim();
161
+ if (!normalizedPattern) {
162
+ continue;
163
+ }
164
+ normalizedPatterns.add(normalizedPattern);
165
+ }
166
+ return [...normalizedPatterns];
167
+ };
168
+ const renderTemplateSection = (marker, content) => `${marker}\n\n${content}`;
169
+ const normalizeTemplateStringSection = (value, fallback) => {
170
+ const normalizedValue = value?.trim();
171
+ return normalizedValue && normalizedValue.length > 0
172
+ ? normalizedValue
173
+ : fallback;
174
+ };
175
+ const normalizeTemplateStringArray = (value, fallback) => {
176
+ if (!value) {
177
+ return [...fallback];
178
+ }
179
+ const normalizedItems = [];
180
+ for (const item of value) {
181
+ const normalizedItem = item.trim();
182
+ if (normalizedItem.length === 0) {
183
+ continue;
184
+ }
185
+ normalizedItems.push(normalizedItem);
186
+ }
187
+ return normalizedItems.length > 0 ? normalizedItems : [...fallback];
188
+ };
137
189
  export function buildGtmTemplateParameters(_options) {
138
- const scopeHelp = buildScopeParameterHelp();
139
- const scopeSelectItems = buildHostConfigurableScopeSelectItems();
140
- const mappingSelectItems = buildGtmConsentTypeSelectItems();
141
190
  return [
142
191
  {
143
192
  type: "TEXT",
144
193
  name: "apiKey",
145
194
  displayName: "API Key",
146
195
  simpleValueType: true,
147
- help: "Find your API key in getuserfeedback.com under Settings → Widget.",
196
+ help: "Paste your getuserfeedback API key from Settings → Widget.",
148
197
  valueValidators: [NON_EMPTY_VALIDATOR],
149
198
  },
150
199
  {
151
200
  type: "GROUP",
152
201
  name: "themeGroup",
153
- displayName: "Theme Settings",
202
+ displayName: "Color Scheme",
154
203
  groupStyle: "ZIPPY_CLOSED",
155
204
  subParams: [
156
205
  {
157
206
  type: "SELECT",
158
207
  name: "themeMode",
159
- displayName: "Theme Mode",
208
+ displayName: "Color Scheme",
160
209
  macrosInSelect: false,
161
210
  selectItems: [
162
211
  {
@@ -182,16 +231,16 @@ export function buildGtmTemplateParameters(_options) {
182
231
  ],
183
232
  simpleValueType: true,
184
233
  defaultValue: "host",
185
- help: "Choose how the widget theme is set: use a fixed mode, follow your site's theme attributes, or read the value from a GTM variable.",
234
+ help: "Choose how the widget color scheme should work.",
186
235
  },
187
236
  {
188
237
  type: "SELECT",
189
238
  name: "themeModeVariable",
190
- displayName: "Theme Mode Variable",
239
+ displayName: "Color Scheme Variable",
191
240
  macrosInSelect: true,
192
241
  selectItems: [],
193
242
  simpleValueType: true,
194
- help: "Use a GTM variable that resolves to `light`, `dark`, `system`, `auto`, or a comma-separated list of attribute names.",
243
+ help: "Choose a GTM variable that returns `light`, `dark`, `system`, `auto`, or a comma-separated list of attribute names.",
195
244
  enablingConditions: [
196
245
  {
197
246
  paramName: "themeMode",
@@ -204,10 +253,10 @@ export function buildGtmTemplateParameters(_options) {
204
253
  {
205
254
  type: "TEXT",
206
255
  name: "themeAttr",
207
- displayName: "Host Theme Attributes",
256
+ displayName: "Site Color Scheme Attributes",
208
257
  simpleValueType: true,
209
258
  defaultValue: DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS.join(","),
210
- help: "Comma-separated attributes to inspect on `<html>` or `<body>` when matching the widget theme to your site.",
259
+ help: "Add the HTML or body attributes that show your site's color scheme, separated by commas.",
211
260
  enablingConditions: [
212
261
  {
213
262
  paramName: "themeMode",
@@ -218,93 +267,6 @@ export function buildGtmTemplateParameters(_options) {
218
267
  },
219
268
  ],
220
269
  },
221
- {
222
- type: "GROUP",
223
- name: "consentGroup",
224
- displayName: "Consent Settings",
225
- groupStyle: "ZIPPY_CLOSED",
226
- subParams: [
227
- {
228
- type: "RADIO",
229
- name: "consentMode",
230
- displayName: "Consent Mode",
231
- radioItems: [
232
- {
233
- value: "automatic",
234
- displayValue: "Automatic (Use GTM Consent Mode)",
235
- help: "Use GTM consent checks to set the widget's initial consent state. This also lets you decide whether analytics measurement should follow analytics storage or use its own fixed setting.",
236
- },
237
- {
238
- value: "manual",
239
- displayValue: "Manual (Map Public Scopes)",
240
- help: "Choose which widget consent settings are granted for each GTM consent type. Required widget storage settings stay enabled and are not configurable here.",
241
- },
242
- ],
243
- simpleValueType: true,
244
- defaultValue: "automatic",
245
- },
246
- {
247
- type: "SELECT",
248
- name: "analyticsMeasurementMode",
249
- displayName: "Analytics Measurement Policy",
250
- macrosInSelect: false,
251
- selectItems: [
252
- {
253
- value: "inherit-analytics-storage",
254
- displayValue: "Inherit analytics_storage",
255
- },
256
- {
257
- value: "granted",
258
- displayValue: "Always granted",
259
- },
260
- {
261
- value: "denied",
262
- displayValue: "Always denied",
263
- },
264
- ],
265
- simpleValueType: true,
266
- defaultValue: "inherit-analytics-storage",
267
- help: [
268
- "Analytics measurement is separate from analytics storage so you can control event measurement independently.",
269
- "Inherit analytics_storage: `analytics.measurement` matches GTM `analytics_storage`.",
270
- "Always granted: `analytics.measurement` is always enabled.",
271
- "Always denied: `analytics.measurement` is always disabled.",
272
- ].join("\n"),
273
- enablingConditions: [...AUTOMATIC_CONSENT_MODE_CONDITION],
274
- },
275
- {
276
- type: "SIMPLE_TABLE",
277
- name: "scopeMappings",
278
- displayName: "Scope Mappings",
279
- simpleTableColumns: [
280
- {
281
- defaultValue: "analytics.storage",
282
- displayName: "Widget Scope",
283
- name: "scope",
284
- type: "SELECT",
285
- isUnique: true,
286
- selectItems: scopeSelectItems,
287
- },
288
- {
289
- defaultValue: "analytics_storage",
290
- displayName: "Granted When",
291
- name: "mapping",
292
- type: "SELECT",
293
- macrosInSelect: true,
294
- selectItems: mappingSelectItems,
295
- },
296
- ],
297
- newRowButtonText: "Add Scope",
298
- help: scopeHelp,
299
- enablingConditions: [...MANUAL_CONSENT_MODE_CONDITION],
300
- valueValidators: [NON_EMPTY_VALIDATOR],
301
- defaultValue: buildHostConfigurableDefaultScopeMappings().map((row) => ({
302
- scope: row.scope,
303
- mapping: row.mapping,
304
- })),
305
- },
306
- ],
307
- },
308
270
  {
309
271
  type: "GROUP",
310
272
  name: "identifyGroup",
@@ -321,44 +283,44 @@ export function buildGtmTemplateParameters(_options) {
321
283
  {
322
284
  value: "intelligent",
323
285
  displayValue: "Intelligent",
324
- help: "Automatically read common data layer keys for user ID, email, and phone. User ID is sent as `identify(userId)` and email or phone stay as raw traits.",
286
+ help: "Automatically read common data layer keys for user ID, email, phone, first name, and last name.",
325
287
  },
326
288
  {
327
289
  value: "disabled",
328
290
  displayValue: "Disabled",
329
- help: "Do not send identify data from this GTM tag.",
291
+ help: "Don't send identity data from this tag.",
330
292
  },
331
293
  {
332
294
  value: "advanced",
333
- displayValue: "Advanced",
334
- help: "Choose how GTM encodes the primary identity and add any extra traits you want to send from GTM variables.",
295
+ displayValue: "Custom",
296
+ help: "Choose the main identity value and add any extra fields you want to send.",
335
297
  },
336
298
  ],
337
299
  },
338
300
  {
339
301
  type: "SELECT",
340
302
  name: "identifyPrimaryIdentityType",
341
- displayName: "Primary identity",
303
+ displayName: "Main identity",
342
304
  simpleValueType: true,
343
305
  macrosInSelect: false,
344
306
  defaultValue: "userId",
345
307
  selectItems: [...ADVANCED_PRIMARY_IDENTITY_SELECT_ITEMS],
346
- help: "Choose whether GTM should encode the primary identity as `identify(userId)` or as a raw identity trait like `email` or `phone`.",
308
+ help: "Choose whether the main identity should be sent as a user ID, email, or phone number.",
347
309
  enablingConditions: [...IDENTIFY_ADVANCED_MODE_CONDITION],
348
310
  },
349
311
  {
350
312
  type: "TEXT",
351
313
  name: "identifyPrimaryIdentityValue",
352
- displayName: "Primary identity value",
314
+ displayName: "Main identity value",
353
315
  simpleValueType: true,
354
- help: "Use a GTM variable that resolves to the selected primary identity value.",
316
+ help: "Choose a GTM variable for the main identity value.",
355
317
  enablingConditions: [...IDENTIFY_ADVANCED_MODE_CONDITION],
356
318
  },
357
319
  {
358
320
  type: "SIMPLE_TABLE",
359
321
  name: "identifyTraits",
360
322
  displayName: "Traits",
361
- help: "Add any traits you want to send from GTM variables. Common examples are `email`, `phone`, `name`, and `plan`. Empty rows are ignored.",
323
+ help: "Add any extra fields you want to send from GTM variables. Common examples are `email`, `phone`, `firstName`, `lastName`, and `plan`. Empty rows are ignored.",
362
324
  newRowButtonText: "Add Trait",
363
325
  simpleTableColumns: [
364
326
  {
@@ -380,8 +342,12 @@ export function buildGtmTemplateParameters(_options) {
380
342
  }
381
343
  export function buildGtmRuntimeSource(input) {
382
344
  const loaderScriptBaseUrl = trimTrailingSlash(input?.loaderScriptBaseUrl ?? DEFAULT_GTM_LOADER_SCRIPT_BASE_URL);
383
- const automaticDefaultScopeMappings = buildAutomaticDefaultScopeMappings();
384
- const defaultTemplateScopeMappings = buildHostConfigurableDefaultScopeMappings();
345
+ const runtimeEndpoints = input?.runtimeEndpoints === undefined
346
+ ? undefined
347
+ : {
348
+ apiUrl: input.runtimeEndpoints.apiUrl.trim(),
349
+ coreUrl: input.runtimeEndpoints.coreUrl.trim(),
350
+ };
385
351
  return `
386
352
  const log = require("logToConsole");
387
353
  const injectScript = require("injectScript");
@@ -393,14 +359,17 @@ const copyFromDataLayer = require("copyFromDataLayer");
393
359
 
394
360
  const loaderScriptBaseUrl = ${JSON.stringify(loaderScriptBaseUrl)};
395
361
  const gtmConsentTypes = ${JSON.stringify(GTM_CONSENT_TYPES)};
396
- const automaticDefaultScopeMappings = ${JSON.stringify(automaticDefaultScopeMappings)};
397
- const defaultScopeMappings = ${JSON.stringify(defaultTemplateScopeMappings)};
398
- const hostConfigurableScopes = ${JSON.stringify(defaultTemplateScopeMappings.map((row) => row.scope))};
399
362
  const defaultAutoDetectColorSchemeAttrs = ${JSON.stringify(DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS)};
400
363
  const queueKey = ${JSON.stringify(GTM_QUEUE_KEY)};
401
364
  const intelligentIdentifyUserIdKeys = ${JSON.stringify(INTELLIGENT_IDENTIFY_USER_ID_KEYS)};
402
365
  const intelligentIdentifyEmailKeys = ${JSON.stringify(INTELLIGENT_IDENTIFY_EMAIL_KEYS)};
366
+ const intelligentIdentifyFirstNameKeys = ${JSON.stringify(INTELLIGENT_IDENTIFY_FIRST_NAME_KEYS)};
367
+ const intelligentIdentifyLastNameKeys = ${JSON.stringify(INTELLIGENT_IDENTIFY_LAST_NAME_KEYS)};
403
368
  const intelligentIdentifyPhoneKeys = ${JSON.stringify(INTELLIGENT_IDENTIFY_PHONE_KEYS)};
369
+ const generatedRuntimeEndpoints = ${JSON.stringify(runtimeEndpoints ?? null)};
370
+ const queueClientMeta = ${JSON.stringify(GTM_QUEUE_CLIENT_META)};
371
+ ${RESOLVE_GTM_THEME_INPUT_FROM_TEMPLATE_DATA_SOURCE}
372
+ ${BUILD_GTM_IDENTIFY_COMMAND_SOURCE}
404
373
 
405
374
  const trimString = function(value) {
406
375
  return typeof value === "string" ? value.trim() : "";
@@ -448,12 +417,29 @@ const normalizeThemeInput = function(inputValue) {
448
417
  return undefined;
449
418
  };
450
419
 
451
- const isListLike = function(value) {
452
- return (
453
- !!value &&
454
- typeof value.length === "number" &&
455
- typeof value.map === "function"
456
- );
420
+ const getSimpleTableRows = function(value) {
421
+ const maxSimpleTableRowCount = 1000;
422
+ const length = value && typeof value === "object" ? value.length : undefined;
423
+ if (
424
+ !value ||
425
+ typeof value !== "object" ||
426
+ typeof length !== "number" ||
427
+ length <= 0 ||
428
+ length > maxSimpleTableRowCount ||
429
+ length % 1 !== 0
430
+ ) {
431
+ return [];
432
+ }
433
+
434
+ const rows = [];
435
+ for (let index = 0; index < length; index += 1) {
436
+ const row = value[index];
437
+ if (!row || typeof row !== "object") {
438
+ continue;
439
+ }
440
+ rows.push(row);
441
+ }
442
+ return rows;
457
443
  };
458
444
 
459
445
  const normalizeTraitValue = function(value) {
@@ -468,12 +454,7 @@ const normalizeTraitValue = function(value) {
468
454
  };
469
455
 
470
456
  const normalizeUserIdValue = function(value) {
471
- if (typeof value !== "string") {
472
- return undefined;
473
- }
474
-
475
- const trimmedValue = value.trim();
476
- return trimmedValue ? trimmedValue : undefined;
457
+ return typeof value === "string" ? trimString(value) || undefined : undefined;
477
458
  };
478
459
 
479
460
  const addTrait = function(result, name, value) {
@@ -503,17 +484,17 @@ const getIntelligentIdentifyValue = function(keys) {
503
484
  };
504
485
 
505
486
  const getIntelligentIdentifyTraits = function() {
506
- const traits = {};
507
- addTrait(traits, "email", getIntelligentIdentifyValue(intelligentIdentifyEmailKeys));
508
- addTrait(traits, "phone", getIntelligentIdentifyValue(intelligentIdentifyPhoneKeys));
509
- return traits;
487
+ return {
488
+ email: getIntelligentIdentifyValue(intelligentIdentifyEmailKeys),
489
+ firstName: getIntelligentIdentifyValue(intelligentIdentifyFirstNameKeys),
490
+ lastName: getIntelligentIdentifyValue(intelligentIdentifyLastNameKeys),
491
+ phone: getIntelligentIdentifyValue(intelligentIdentifyPhoneKeys),
492
+ };
510
493
  };
511
494
 
512
495
  const getAdvancedIdentifyTraits = function(templateData) {
513
496
  const traits = {};
514
- const rows = isListLike(templateData.identifyTraits)
515
- ? templateData.identifyTraits
516
- : [];
497
+ const rows = getSimpleTableRows(templateData.identifyTraits);
517
498
  for (let index = 0; index < rows.length; index += 1) {
518
499
  const row = rows[index];
519
500
  if (!row || typeof row !== "object") {
@@ -524,96 +505,27 @@ const getAdvancedIdentifyTraits = function(templateData) {
524
505
  return traits;
525
506
  };
526
507
 
527
- const hasOwnKeys = function(value) {
528
- for (const key in value) {
529
- return !!key;
530
- }
531
- return false;
532
- };
533
-
534
- const getIdentifyCommandFromUserIdAndTraits = function(userId, traits) {
535
- const hasTraits = hasOwnKeys(traits);
536
-
537
- if (userId && hasTraits) {
538
- return {
539
- kind: "identify",
540
- userId: userId,
541
- traits: traits,
542
- };
543
- }
544
- if (userId) {
545
- return {
546
- kind: "identify",
547
- userId: userId,
548
- };
549
- }
550
- if (hasTraits) {
551
- return {
552
- kind: "identify",
553
- traits: traits,
554
- };
555
- }
556
- return undefined;
557
- };
558
-
559
- const getIntelligentIdentifyCommand = function() {
560
- const userId = normalizeUserIdValue(
561
- getIntelligentIdentifyValue(intelligentIdentifyUserIdKeys),
562
- );
563
- const traits = getIntelligentIdentifyTraits();
564
- return getIdentifyCommandFromUserIdAndTraits(userId, traits);
565
- };
566
-
567
- const getAdvancedIdentifyCommand = function(templateData) {
568
- const traits = getAdvancedIdentifyTraits(templateData);
569
- const primaryIdentityType = trimString(templateData.identifyPrimaryIdentityType);
570
- const primaryIdentityValue = normalizeTraitValue(
571
- templateData.identifyPrimaryIdentityValue,
572
- );
573
-
574
- if (primaryIdentityType === "userId") {
575
- const userId = normalizeUserIdValue(primaryIdentityValue);
576
- return getIdentifyCommandFromUserIdAndTraits(userId, traits);
577
- }
578
-
579
- if (
580
- primaryIdentityType === "email" ||
581
- primaryIdentityType === "phone"
582
- ) {
583
- addTrait(traits, primaryIdentityType, primaryIdentityValue);
584
- }
585
-
586
- return hasOwnKeys(traits)
587
- ? {
588
- kind: "identify",
589
- traits: traits,
590
- }
591
- : undefined;
592
- };
593
-
594
508
  const getIdentifyCommand = function(templateData) {
595
509
  if (templateData.identifyMode === "intelligent") {
596
- return getIntelligentIdentifyCommand();
510
+ const intelligentTraits = getIntelligentIdentifyTraits();
511
+ return buildGtmIdentifyCommand({
512
+ email: intelligentTraits.email,
513
+ firstName: intelligentTraits.firstName,
514
+ lastName: intelligentTraits.lastName,
515
+ mode: "intelligent",
516
+ phone: intelligentTraits.phone,
517
+ userId: getIntelligentIdentifyValue(intelligentIdentifyUserIdKeys),
518
+ });
597
519
  }
598
520
  if (templateData.identifyMode === "advanced") {
599
- return getAdvancedIdentifyCommand(templateData);
600
- }
601
- return undefined;
602
- };
603
-
604
- const getThemeInput = function(templateData) {
605
- if (templateData.themeMode === "variable") {
606
- return templateData.themeModeVariable;
607
- }
608
- if (templateData.themeMode === "host") {
609
- return templateData.themeAttr;
610
- }
611
- if (
612
- templateData.themeMode === "light" ||
613
- templateData.themeMode === "dark" ||
614
- templateData.themeMode === "system"
615
- ) {
616
- return templateData.themeMode;
521
+ return buildGtmIdentifyCommand({
522
+ mode: "advanced",
523
+ primaryIdentityType: trimString(templateData.identifyPrimaryIdentityType),
524
+ primaryIdentityValue: normalizeTraitValue(
525
+ templateData.identifyPrimaryIdentityValue,
526
+ ),
527
+ traits: getAdvancedIdentifyTraits(templateData),
528
+ });
617
529
  }
618
530
  return undefined;
619
531
  };
@@ -629,147 +541,78 @@ const getConsentState = function() {
629
541
  return result;
630
542
  };
631
543
 
632
- const dedupeScopes = function(scopes) {
633
- const seen = {};
634
- const result = [];
635
- for (let index = 0; index < scopes.length; index += 1) {
636
- const scope = scopes[index];
637
- if (typeof scope !== "string" || seen[scope]) {
638
- continue;
639
- }
640
- seen[scope] = true;
641
- result.push(scope);
642
- }
643
- return result;
644
- };
645
-
646
- const isKnownConsentType = function(consentType) {
647
- for (let index = 0; index < gtmConsentTypes.length; index += 1) {
648
- if (gtmConsentTypes[index] === consentType) {
649
- return true;
650
- }
651
- }
652
- return false;
653
- };
654
-
655
- const isHostConfigurableScope = function(scope) {
656
- for (let index = 0; index < hostConfigurableScopes.length; index += 1) {
657
- if (hostConfigurableScopes[index] === scope) {
658
- return true;
659
- }
660
- }
661
- return false;
662
- };
663
-
664
- const getManualScopeMappings = function(templateData) {
665
- const rawRows = isListLike(templateData.scopeMappings)
666
- ? templateData.scopeMappings
667
- : [];
668
- const rows = rawRows.length > 0 ? rawRows : defaultScopeMappings;
669
- return rows
670
- .map(function(row) {
671
- if (!row || typeof row !== "object") {
672
- return undefined;
673
- }
674
- const scope = trimString(row.scope);
675
- const consentType = trimString(row.mapping);
676
- if (!scope || !consentType) {
677
- return undefined;
678
- }
679
- if (!isHostConfigurableScope(scope) || !isKnownConsentType(consentType)) {
680
- return undefined;
681
- }
682
- return {
683
- scope: scope,
684
- consentType: consentType,
685
- };
686
- })
687
- .filter(function(row) {
688
- return row !== undefined;
689
- });
690
- };
691
-
692
- const getAnalyticsMeasurementMode = function(templateData) {
693
- if (
694
- templateData.analyticsMeasurementMode === "granted" ||
695
- templateData.analyticsMeasurementMode === "denied"
696
- ) {
697
- return templateData.analyticsMeasurementMode;
698
- }
699
- return "inherit-analytics-storage";
700
- };
701
-
702
544
  const buildDefaultConsent = function(templateData) {
703
545
  const consentState = getConsentState();
546
+ const grantedScopeLookup = {};
704
547
  const grantedScopes = [];
705
-
706
- if (templateData.consentMode === "manual") {
707
- const scopeMappings = getManualScopeMappings(templateData);
708
- for (let index = 0; index < scopeMappings.length; index += 1) {
709
- const mapping = scopeMappings[index];
710
- if (consentState[mapping.consentType] === "granted") {
711
- grantedScopes.push(mapping.scope);
712
- }
713
- }
714
- } else {
715
- for (let index = 0; index < automaticDefaultScopeMappings.length; index += 1) {
716
- const mapping = automaticDefaultScopeMappings[index];
717
- if (consentState[mapping.mapping] === "granted") {
718
- grantedScopes.push(mapping.scope);
719
- }
548
+ const grantScope = function(scope) {
549
+ if (!scope || grantedScopeLookup[scope]) {
550
+ return;
720
551
  }
552
+ grantedScopeLookup[scope] = true;
553
+ grantedScopes.push(scope);
554
+ };
721
555
 
722
- const analyticsMeasurementMode = getAnalyticsMeasurementMode(templateData);
723
- if (analyticsMeasurementMode === "granted") {
724
- grantedScopes.push("analytics.measurement");
725
- }
726
- if (
727
- analyticsMeasurementMode === "inherit-analytics-storage" &&
728
- consentState.analytics_storage === "granted"
729
- ) {
730
- grantedScopes.push("analytics.measurement");
731
- }
556
+ if (consentState.analytics_storage === "granted") {
557
+ grantScope("analytics.storage");
558
+ grantScope("analytics.measurement");
559
+ }
560
+ if (consentState.personalization_storage === "granted") {
561
+ grantScope("personalization.storage");
562
+ }
563
+ if (consentState.ad_storage === "granted") {
564
+ grantScope("ads.storage");
565
+ }
566
+ if (consentState.ad_user_data === "granted") {
567
+ grantScope("ads.user_data");
568
+ }
569
+ if (consentState.ad_personalization === "granted") {
570
+ grantScope("ads.personalization");
732
571
  }
733
572
 
734
- const dedupedScopes = dedupeScopes(grantedScopes);
735
- return dedupedScopes.length > 0 ? dedupedScopes : undefined;
573
+ return grantedScopes.length > 0 ? grantedScopes : undefined;
736
574
  };
737
575
 
738
- const onSuccess = function() {
739
- if (typeof data.gtmOnSuccess === "function") {
740
- data.gtmOnSuccess();
741
- }
576
+ let requestSequence = 0;
577
+
578
+ const enqueuePublicCommand = function(queuePush, instanceId, command) {
579
+ requestSequence += 1;
580
+ const requestId = "gtm_" + command.kind + "_" + requestSequence;
581
+ queuePush({
582
+ version: "1",
583
+ instanceId: instanceId,
584
+ requestId: requestId,
585
+ idempotencyKey: requestId,
586
+ clientMeta: queueClientMeta,
587
+ command: command,
588
+ });
742
589
  };
743
590
 
744
- const onFailure = function() {
591
+ const apiKey = trimString(data.apiKey);
592
+ if (!apiKey) {
745
593
  if (typeof data.gtmOnFailure === "function") {
746
594
  data.gtmOnFailure();
747
595
  }
748
- };
749
-
750
- const apiKey = trimString(data.apiKey);
751
- if (!apiKey) {
752
- onFailure();
753
596
  return;
754
597
  }
755
598
 
756
599
  const encodedApiKey = encodeUriComponent(apiKey);
757
600
  if (!encodedApiKey) {
758
- onFailure();
601
+ if (typeof data.gtmOnFailure === "function") {
602
+ data.gtmOnFailure();
603
+ }
759
604
  return;
760
605
  }
761
-
762
606
  const widgetUrl = loaderScriptBaseUrl + "/" + encodedApiKey + "/loader.js";
763
607
  const queuePush = createQueue(queueKey);
608
+ const instanceId = "gtm-" + encodedApiKey;
764
609
  const initOptions = {
765
610
  apiKey: apiKey,
766
- clientMeta: {
767
- loader: "gtm",
768
- transport: "tag-manager",
769
- },
611
+ clientMeta: queueClientMeta,
770
612
  };
771
613
 
772
- const colorScheme = normalizeThemeInput(getThemeInput(data));
614
+ const themeInput = resolveGtmThemeInputFromTemplateData(data);
615
+ const colorScheme = normalizeThemeInput(themeInput);
773
616
  if (colorScheme !== undefined) {
774
617
  initOptions.colorScheme = colorScheme;
775
618
  }
@@ -779,33 +622,50 @@ if (defaultConsent !== undefined) {
779
622
  initOptions.defaultConsent = defaultConsent;
780
623
  }
781
624
 
782
- queuePush({
625
+ if (generatedRuntimeEndpoints !== null) {
626
+ initOptions.runtimeEndpoints = {
627
+ apiUrl: generatedRuntimeEndpoints.apiUrl,
628
+ coreUrl: generatedRuntimeEndpoints.coreUrl,
629
+ };
630
+ }
631
+
632
+ enqueuePublicCommand(queuePush, instanceId, {
783
633
  kind: "init",
784
634
  opts: initOptions,
785
635
  });
786
636
 
787
637
  const identifyCommand = getIdentifyCommand(data);
788
638
  if (identifyCommand !== undefined) {
789
- queuePush(identifyCommand);
639
+ enqueuePublicCommand(queuePush, instanceId, identifyCommand);
790
640
  }
791
641
 
792
642
  log("Loading getuserfeedback loader:", widgetUrl);
793
643
 
794
644
  if (queryPermission("inject_script", widgetUrl)) {
795
- injectScript(widgetUrl, onSuccess, onFailure, "getuserfeedback");
645
+ injectScript(
646
+ widgetUrl,
647
+ typeof data.gtmOnSuccess === "function" ? data.gtmOnSuccess : undefined,
648
+ typeof data.gtmOnFailure === "function" ? data.gtmOnFailure : undefined,
649
+ "getuserfeedback",
650
+ );
796
651
  } else {
797
- onFailure();
652
+ if (typeof data.gtmOnFailure === "function") {
653
+ data.gtmOnFailure();
654
+ }
798
655
  }
799
656
  `.trim();
800
657
  }
801
658
  export function buildGtmPermissions(input) {
802
659
  const loaderScriptBaseUrl = trimTrailingSlash(input?.loaderScriptBaseUrl ?? DEFAULT_GTM_LOADER_SCRIPT_BASE_URL);
803
- const loaderUrlPattern = `${loaderScriptBaseUrl}/*/loader.js`;
660
+ const loaderUrlPatterns = [
661
+ `${loaderScriptBaseUrl}/*/loader.js`,
662
+ ...normalizeAdditionalLoaderScriptUrlPatterns(input?.additionalLoaderScriptUrlPatterns),
663
+ ];
804
664
  return [
805
665
  buildPermission("inject_script", [
806
666
  {
807
667
  key: "urls",
808
- value: listValue([stringValue(loaderUrlPattern)]),
668
+ value: listValue(loaderUrlPatterns.map((pattern) => stringValue(pattern))),
809
669
  },
810
670
  ]),
811
671
  buildPermission("logging", [
@@ -843,6 +703,8 @@ export function buildGtmPermissions(input) {
843
703
  value: listValue([
844
704
  ...INTELLIGENT_IDENTIFY_USER_ID_KEYS,
845
705
  ...INTELLIGENT_IDENTIFY_EMAIL_KEYS,
706
+ ...INTELLIGENT_IDENTIFY_FIRST_NAME_KEYS,
707
+ ...INTELLIGENT_IDENTIFY_LAST_NAME_KEYS,
846
708
  ...INTELLIGENT_IDENTIFY_PHONE_KEYS,
847
709
  ].map((key) => stringValue(key))),
848
710
  },
@@ -870,3 +732,32 @@ export function buildGtmTemplateParts(input) {
870
732
  export function buildGtmTemplateArtifact(input) {
871
733
  return buildGtmTemplateParts(input);
872
734
  }
735
+ export function buildGtmTemplateFileSource(input) {
736
+ const artifact = buildGtmTemplateArtifact(input);
737
+ const infoOverrides = input?.infoOverrides;
738
+ const info = {
739
+ type: artifact.info.type,
740
+ id: normalizeTemplateStringSection(infoOverrides?.id, GTM_TEMPLATE_DEFAULT_ID),
741
+ version: artifact.info.version,
742
+ securityGroups: normalizeTemplateStringArray(infoOverrides?.securityGroups, []),
743
+ displayName: normalizeTemplateStringSection(infoOverrides?.displayName, GTM_TEMPLATE_GALLERY_DISPLAY_NAME),
744
+ categories: normalizeTemplateStringArray(infoOverrides?.categories, GTM_TEMPLATE_GALLERY_CATEGORIES),
745
+ brand: {
746
+ displayName: normalizeTemplateStringSection(infoOverrides?.brandDisplayName, GTM_TEMPLATE_DEFAULT_BRAND_DISPLAY_NAME),
747
+ thumbnail: normalizeTemplateStringSection(infoOverrides?.brandThumbnail, GTM_TEMPLATE_DEFAULT_BRAND_THUMBNAIL),
748
+ },
749
+ description: normalizeTemplateStringSection(infoOverrides?.description, GTM_TEMPLATE_GALLERY_DESCRIPTION),
750
+ containerContexts: [...artifact.info.containerContexts],
751
+ };
752
+ const sections = [
753
+ renderTemplateSection("___TERMS_OF_SERVICE___", normalizeTemplateStringSection(input?.termsOfService, GTM_TEMPLATE_GALLERY_TERMS_OF_SERVICE)),
754
+ renderTemplateSection("___INFO___", JSON.stringify(info, null, 2)),
755
+ renderTemplateSection("___TEMPLATE_PARAMETERS___", JSON.stringify(artifact.parameters, null, 2)),
756
+ renderTemplateSection("___SANDBOXED_JS_FOR_WEB_TEMPLATE___", artifact.runtimeSource),
757
+ renderTemplateSection("___WEB_PERMISSIONS___", JSON.stringify(artifact.permissions, null, 2)),
758
+ renderTemplateSection("___TESTS___", normalizeTemplateStringSection(input?.tests, GTM_TEMPLATE_DEFAULT_TESTS)),
759
+ renderTemplateSection("___NOTES___", normalizeTemplateStringSection(input?.notes, GTM_TEMPLATE_DEFAULT_NOTES)),
760
+ "",
761
+ ];
762
+ return `${input?.includeUtf8Bom === false ? "" : GTM_TEMPLATE_UTF8_BOM}${sections.join("\n\n")}`;
763
+ }