@mparticle/web-sdk 3.30.0 → 3.31.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.
@@ -96,7 +96,7 @@ const steps = new RegExp(
96
96
  'ir a caja|continuar al pago|tramitar pedido',
97
97
  "procedi al checkout|procedi all'acquisto|procedi al pagamento|vai alla cassa",
98
98
  'afrekenen|verder naar betalen|doorgaan naar betalen',
99
- 'レジに進む|ご?(?:購入|注文)手続き(?:へ|に)(?:進む)?|お?支払い(?:へ|に)進む|(?:ご注文内容の)?確認画面へ(?:進む)?',
99
+ 'レジに進む|ご?(?:購入|注文)手続き(?:(?:へ|に)(?:進む)?)?|お?支払い(?:へ|に)進む|(?:ご注文内容の)?確認画面へ(?:進む)?',
100
100
  ].join('|') +
101
101
  ')$'
102
102
  );
@@ -280,7 +280,34 @@ export type CheckoutIntentSource =
280
280
  | 'frame'
281
281
  | 'submit'
282
282
  | 'name'
283
- | 'api';
283
+ | 'api'
284
+ | 'selector';
285
+
286
+ // CSS selectors a kit supplies for the page's own confirm and checkout-step
287
+ // buttons. A control matching one counts as that kind before any label rule,
288
+ // except that a confirm, wallet or add-to-cart label beats a checkout-step
289
+ // selector.
290
+ export interface ICheckoutIntentSelectors {
291
+ commit?: readonly string[];
292
+ checkout_step?: readonly string[];
293
+ }
294
+
295
+ // A selector the browser can't parse is skipped rather than thrown.
296
+ function matchesAnySelector(
297
+ control: Element,
298
+ selectors: readonly string[] | undefined
299
+ ): boolean {
300
+ if (!selectors) {
301
+ return false;
302
+ }
303
+ return selectors.some(selector => {
304
+ try {
305
+ return control.matches(selector);
306
+ } catch (e) {
307
+ return false;
308
+ }
309
+ });
310
+ }
284
311
 
285
312
  export interface ICheckoutIntent {
286
313
  kind: Exclude<CheckoutIntentKind, 'frame'>;
@@ -328,13 +355,17 @@ function isCommitLabel(text: string, isLink: boolean, pathname: string, control:
328
355
 
329
356
  // Classifies a clicked or submitting control found by findControl, in order:
330
357
  // pickers, toggles and pay-later messages never count; a vendor's own wallet
331
- // button counts before any label rule; a confirm label on any source counts
332
- // before the look-alike exclusions veto; then a wallet-named control, wallet
333
- // phrases and steps. An add-to-cart label, links included, or a control that
334
- // opens the cart on the page counts only where none of those do.
358
+ // button keeps its vendor kind; a control matching a kit-supplied commit
359
+ // selector counts as a commit before any label rule, and one matching a
360
+ // checkout-step selector as a step unless its labels make it a confirm, a
361
+ // wallet or an add-to-cart click; a confirm label on any source
362
+ // counts before the look-alike exclusions veto; then a wallet-named control,
363
+ // wallet phrases and steps. An add-to-cart label, links included, or a control
364
+ // that opens the cart on the page counts only where none of those do.
335
365
  export function classifyIntent(
336
366
  control: Element,
337
- pathname: string
367
+ pathname: string,
368
+ selectors?: ICheckoutIntentSelectors
338
369
  ): ICheckoutIntent | undefined {
339
370
  const vendor = vendorButtonKind(control);
340
371
  if (vendor === 'none') {
@@ -345,6 +376,30 @@ export function classifyIntent(
345
376
  ? undefined
346
377
  : { kind: vendor, source: 'vendor' };
347
378
  }
379
+ if (selectors && !isQuietIntentControl(control, false)) {
380
+ if (matchesAnySelector(control, selectors.commit)) {
381
+ return { kind: 'commit', source: 'selector' };
382
+ }
383
+ if (matchesAnySelector(control, selectors.checkout_step)) {
384
+ // A confirm, wallet or add-to-cart label still wins, so a class those
385
+ // buttons share with a step button can't turn their clicks into steps.
386
+ const byLabel = classifyLabels(control, pathname);
387
+ return byLabel &&
388
+ (byLabel.kind === 'commit' ||
389
+ byLabel.kind === 'wallet' ||
390
+ byLabel.adds === true)
391
+ ? byLabel
392
+ : { kind: 'checkout_step', source: 'selector' };
393
+ }
394
+ }
395
+ return classifyLabels(control, pathname);
396
+ }
397
+
398
+ // The label rules, read once the vendor and selector checks have passed.
399
+ function classifyLabels(
400
+ control: Element,
401
+ pathname: string
402
+ ): ICheckoutIntent | undefined {
348
403
  const texts = labels(control);
349
404
  const cores = texts.map(coreLabel);
350
405
  const cartAction = cartControlAction(control, cores);
package/src/ecommerce.ts CHANGED
@@ -369,7 +369,7 @@ export default function Ecommerce(
369
369
  );
370
370
  return null;
371
371
  } else {
372
- price = mpInstance._Helpers.parseNumber(price);
372
+ price = parseNumber(price);
373
373
  }
374
374
 
375
375
  if (position && !mpInstance._Helpers.Validators.isNumber(position)) {
@@ -382,7 +382,7 @@ export default function Ecommerce(
382
382
  if (!mpInstance._Helpers.Validators.isStringOrNumber(quantity)) {
383
383
  quantity = 1;
384
384
  } else {
385
- quantity = mpInstance._Helpers.parseNumber(quantity);
385
+ quantity = parseNumber(quantity);
386
386
  }
387
387
 
388
388
  return {
@@ -680,6 +680,6 @@ export default function Ecommerce(
680
680
  }
681
681
 
682
682
  // if amount is a string, it will be parsed into a number if possible, or set to 0
683
- return mpInstance._Helpers.parseNumber(amount);
683
+ return parseNumber(amount);
684
684
  };
685
685
  }
package/src/events.ts CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  SDKEventOptions,
21
21
  TransactionAttributes,
22
22
  } from '@mparticle/web-sdk';
23
- import { getHref, valueof } from './utils';
23
+ import { getHref, valueof, isObject } from './utils';
24
24
  import {
25
25
  allowedQueryParams,
26
26
  autoPageViewAttribute,
@@ -242,7 +242,7 @@ export default function Events(
242
242
  );
243
243
  }
244
244
 
245
- if (mpInstance._Helpers.isObject(transactionAttributes)) {
245
+ if (isObject(transactionAttributes)) {
246
246
  mpInstance._Ecommerce.convertTransactionAttributesToProductAction(
247
247
  transactionAttributes,
248
248
  event.ProductAction
package/src/forwarders.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import filteredMparticleUser, {
2
2
  isIdentityAllowed,
3
3
  } from './filteredMparticleUser';
4
- import { isEmpty, extend, hasOwnProp, Dictionary } from './utils';
4
+ import { isEmpty, extend, hasOwnProp, Dictionary, isObject } from './utils';
5
5
  import KitFilterHelper from './kitFilterHelper';
6
6
  import Constants from './constants';
7
7
  import APIClient, { IForwardingStatsData } from './apiClient';
@@ -197,7 +197,7 @@ export default function Forwarders(
197
197
  ): boolean {
198
198
  if (
199
199
  !filterObject ||
200
- !mpInstance._Helpers.isObject(filterObject) ||
200
+ !isObject(filterObject) ||
201
201
  !Object.keys(filterObject).length
202
202
  ) {
203
203
  return true;
@@ -212,7 +212,7 @@ export default function Forwarders(
212
212
  try {
213
213
  const hasUserAttributes =
214
214
  userAttributes &&
215
- mpInstance._Helpers.isObject(userAttributes) &&
215
+ isObject(userAttributes) &&
216
216
  Object.keys(userAttributes).length > 0;
217
217
 
218
218
  const isMatch = hasUserAttributes
package/src/identity.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  isReservedStoredPropertyName,
21
21
  isFunction,
22
22
  isObject,
23
+ findKeyInObject,
23
24
  } from './utils';
24
25
  import { getIdentityTypeFromStoredKey } from './types';
25
26
  import { hasMPIDAndUserLoginChanged, hasMPIDChanged } from './user-utils';
@@ -990,7 +991,7 @@ export default function Identity(
990
991
  let previousUserAttributeValue;
991
992
  let isNewAttribute;
992
993
 
993
- const existingProp = mpInstance._Helpers.findKeyInObject(
994
+ const existingProp = findKeyInObject(
994
995
  userAttributes,
995
996
  key
996
997
  );
@@ -1080,7 +1081,7 @@ export default function Identity(
1080
1081
 
1081
1082
  userAttributes = this.getAllUserAttributes();
1082
1083
 
1083
- const existingProp = mpInstance._Helpers.findKeyInObject(
1084
+ const existingProp = findKeyInObject(
1084
1085
  userAttributes,
1085
1086
  key
1086
1087
  );
@@ -1157,7 +1158,7 @@ export default function Identity(
1157
1158
  let isNewAttribute;
1158
1159
  let userAttributeChange;
1159
1160
 
1160
- const existingProp = mpInstance._Helpers.findKeyInObject(
1161
+ const existingProp = findKeyInObject(
1161
1162
  userAttributes,
1162
1163
  key
1163
1164
  );
@@ -35,6 +35,7 @@ export type {
35
35
  IRoktKitSettings,
36
36
  IRoktKit,
37
37
  ICheckoutIntentSignal,
38
+ ICheckoutIntentSelectors,
38
39
  IRoktOptions,
39
40
  IRoktLauncherOptions,
40
41
  } from './roktManager';
@@ -4,6 +4,7 @@ import { BaseEvent, EventTypeEnum, CommerceEvent, ScreenViewEvent, CustomEvent }
4
4
  import Types from './types'
5
5
  import { DataPlanPoint } from '@mparticle/data-planning-models';
6
6
  import { IMParticleWebSDKInstance } from './mp-instance';
7
+ import { isObject, parseNumber } from './utils';
7
8
 
8
9
  /*
9
10
  TODO: Including this as a workaround because attempting to import it from
@@ -476,7 +477,7 @@ export default class KitBlocker {
476
477
  const clonedEvent = {...event};
477
478
  if (this.blockUserAttributes) {
478
479
  const matchedAttributes = this.dataPlanMatchLookups['user_attributes'];
479
- if (this.mpInstance._Helpers.isObject(matchedAttributes) && clonedEvent.UserAttributes) {
480
+ if (isObject(matchedAttributes) && clonedEvent.UserAttributes) {
480
481
  const plannedUserAttributes = {};
481
482
  for (const ua of Object.keys(clonedEvent.UserAttributes)) {
482
483
  if (matchedAttributes[ua] === true) {
@@ -545,11 +546,11 @@ export default class KitBlocker {
545
546
 
546
547
  if (this.blockUserIdentities) {
547
548
  const matchedIdentities = this.dataPlanMatchLookups['user_identities'];
548
- if (this.mpInstance._Helpers.isObject(matchedIdentities)) {
549
+ if (isObject(matchedIdentities)) {
549
550
  if (clonedEvent?.UserIdentities?.length) {
550
551
  clonedEvent.UserIdentities = clonedEvent.UserIdentities.filter(uiByType => {
551
552
  const identityName = Types.IdentityType.getIdentityName(
552
- this.mpInstance._Helpers.parseNumber(uiByType.Type)
553
+ parseNumber(uiByType.Type)
553
554
  );
554
555
 
555
556
  return matchedIdentities[identityName];
@@ -39,7 +39,7 @@ import Consent, { IConsent } from './consent';
39
39
  import KitBlocker from './kitBlocking';
40
40
  import ConfigAPIClient, { IKitConfigs } from './configAPIClient';
41
41
  import IdentityAPIClient, { IIdentityApiClient } from './identityApiClient';
42
- import { isFunction, parseConfig, valueof, generateDeprecationMessage, extend } from './utils';
42
+ import { isFunction, parseConfig, valueof, generateDeprecationMessage, extend, findKeyInObject, isObject } from './utils';
43
43
  import { DisabledVault, LocalStorageVault } from './vault';
44
44
  import { removeExpiredIdentityCacheDates, hasExplicitIdentifier } from './identity-utils';
45
45
  import IntegrationCapture, { isIntegrationCaptureEnabled } from './integrationCapture';
@@ -703,7 +703,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
703
703
  hostname: window.location.hostname,
704
704
  title: window.document.title,
705
705
  };
706
- } else if (!self._Helpers.isObject(attrs)) {
706
+ } else if (!isObject(attrs)) {
707
707
  self.Logger.error(
708
708
  'The attributes argument must be an object. A ' +
709
709
  typeof attrs +
@@ -711,7 +711,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
711
711
  );
712
712
  return;
713
713
  }
714
- if (customFlags && !self._Helpers.isObject(customFlags)) {
714
+ if (customFlags && !isObject(customFlags)) {
715
715
  self.Logger.error(
716
716
  'The customFlags argument must be an object. A ' +
717
717
  typeof customFlags +
@@ -1108,7 +1108,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
1108
1108
  JSON.stringify({ key: key, value: value })
1109
1109
  );
1110
1110
  } else {
1111
- const existingProp = self._Helpers.findKeyInObject(
1111
+ const existingProp = findKeyInObject(
1112
1112
  self._Store.sessionAttributes,
1113
1113
  key
1114
1114
  );
@@ -1190,7 +1190,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
1190
1190
  }
1191
1191
  if (attrs === null) {
1192
1192
  self._Store.integrationAttributes[integrationId] = {};
1193
- } else if (self._Helpers.isObject(attrs)) {
1193
+ } else if (isObject(attrs)) {
1194
1194
  if (Object.keys(attrs).length === 0) {
1195
1195
  self._Store.integrationAttributes[integrationId] = {};
1196
1196
  } else {
@@ -1198,7 +1198,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
1198
1198
  if (typeof key === 'string') {
1199
1199
  if (typeof attrs[key] === 'string') {
1200
1200
  if (
1201
- self._Helpers.isObject(
1201
+ isObject(
1202
1202
  self._Store.integrationAttributes[
1203
1203
  integrationId
1204
1204
  ]
@@ -5,7 +5,7 @@ import { IMParticleWebSDKInstance } from './mp-instance';
5
5
  import { IPersistence, IPersistenceMinified } from './persistence.interfaces';
6
6
  import { MPID } from '@mparticle/web-sdk';
7
7
  import { CookieSyncDates } from './cookieSyncManager';
8
- import { Dictionary } from './utils';
8
+ import { Dictionary, isObject } from './utils';
9
9
  import { IMParticleInstanceManager } from './sdkRuntimeModels';
10
10
 
11
11
  const Base64 = Polyfill.Base64,
@@ -370,7 +370,7 @@ export default function _Persistence(
370
370
  return null;
371
371
  }
372
372
  const record = JSON.parse(decoded);
373
- return mpInstance._Helpers.isObject(record) &&
373
+ return isObject(record) &&
374
374
  Object.keys(record).length
375
375
  ? (record as IPersistenceMinified)
376
376
  : null;
@@ -737,7 +737,7 @@ export default function _Persistence(
737
737
  if (Array.isArray(value)) {
738
738
  return value.length > 0;
739
739
  }
740
- if (!mpInstance._Helpers.isObject(value)) {
740
+ if (!isObject(value)) {
741
741
  return false;
742
742
  }
743
743
  return Object.keys(value).length > 0;
@@ -773,7 +773,7 @@ export default function _Persistence(
773
773
  function encodeMpidBase64Field(container, key: string): void {
774
774
  const value = container[key];
775
775
  if (
776
- mpInstance._Helpers.isObject(value) &&
776
+ isObject(value) &&
777
777
  Object.keys(value).length
778
778
  ) {
779
779
  container[key] = Base64.encode(JSON.stringify(value));
@@ -855,7 +855,7 @@ export default function _Persistence(
855
855
  continue;
856
856
  }
857
857
  if (!SDKv2NonMPIDCookieKeys[mpid]) {
858
- if (!mpInstance._Helpers.isObject(persistence[mpid])) {
858
+ if (!isObject(persistence[mpid])) {
859
859
  delete persistence[mpid];
860
860
  continue;
861
861
  }
@@ -881,7 +881,7 @@ export default function _Persistence(
881
881
  Utils.revertCookieString(persistenceString)
882
882
  ) as IPersistenceMinified;
883
883
  if (
884
- mpInstance._Helpers.isObject(persistence) &&
884
+ isObject(persistence) &&
885
885
  Object.keys(persistence).length
886
886
  ) {
887
887
  decodeGlobalSettings(persistence.gs);
@@ -935,7 +935,7 @@ export default function _Persistence(
935
935
  if (csd) {
936
936
  let persistence = self.getPersistence();
937
937
  if (persistence) {
938
- if (mpInstance._Helpers.isObject(persistence[mpid])) {
938
+ if (isObject(persistence[mpid])) {
939
939
  persistence[mpid].csd = csd;
940
940
  } else {
941
941
  persistence[mpid] = {
@@ -1053,7 +1053,7 @@ export default function _Persistence(
1053
1053
  }
1054
1054
  let persistence = self.getPersistence();
1055
1055
  if (persistence) {
1056
- if (!mpInstance._Helpers.isObject(persistence[mpid])) {
1056
+ if (!isObject(persistence[mpid])) {
1057
1057
  persistence[mpid] = {};
1058
1058
  }
1059
1059
  if (!persistence[mpid].fst) {
@@ -1093,7 +1093,7 @@ export default function _Persistence(
1093
1093
  time = new Date().getTime();
1094
1094
  }
1095
1095
  let persistence = self.getPersistence();
1096
- if (persistence && mpInstance._Helpers.isObject(persistence[mpid])) {
1096
+ if (persistence && isObject(persistence[mpid])) {
1097
1097
  persistence[mpid].lst = time;
1098
1098
  self.savePersistence(persistence);
1099
1099
  }
@@ -6,6 +6,7 @@ export const ErrorCodes = {
6
6
  IDENTITY_REQUEST: 'IDENTITY_REQUEST',
7
7
  IDENTITY_MISMATCH: 'IDENTITY_MISMATCH',
8
8
  MP_DEPRECATED_METHOD_USAGE: 'MP_DEPRECATED_METHOD_USAGE',
9
+ SELECT_PLACEMENTS_IDENTITY_IN_FLIGHT: 'SELECT_PLACEMENTS_IDENTITY_IN_FLIGHT',
9
10
  } as const;
10
11
 
11
12
  export type ErrorCodes = valueof<typeof ErrorCodes>;
@@ -23,7 +23,8 @@ import { PARTNER_MODULE_IDS } from "./cookieSyncManager";
23
23
  import IntegrationCapture from "./integrationCapture";
24
24
  import Constants from "./constants";
25
25
  import { subscribeToRouteChange } from "./routeChangeMonitor";
26
- import type { CheckoutIntentKind, CheckoutIntentSource, PaymentFrameFamily } from "./buttonTracking/checkoutIntent";
26
+ import type { CheckoutIntentKind, CheckoutIntentSource, ICheckoutIntentSelectors, PaymentFrameFamily } from "./buttonTracking/checkoutIntent";
27
+ export type { ICheckoutIntentSelectors } from "./buttonTracking/checkoutIntent";
27
28
 
28
29
  const PASSBACK_CONVERSION_TRACKING_ID = 'passbackconversiontrackingid';
29
30
  // One signal per kind (commit, wallet, checkout_step, frame and cart), with an
@@ -123,6 +124,9 @@ export interface IRoktKit {
123
124
  onRouteChange?: () => void;
124
125
  // Set by the kit only when it wants checkout intent signals.
125
126
  onCheckoutIntent?: (signal: ICheckoutIntentSignal) => void;
127
+ // Set by the kit when its config names the page's confirm or checkout-step
128
+ // buttons. Older kits leave it unset, and the label rules apply alone.
129
+ checkoutIntentSelectors?: ICheckoutIntentSelectors;
126
130
  // Set by the kit only when it wants payment picks.
127
131
  onPaymentPick?: (signal: IPaymentPickSignal) => void;
128
132
  launcherOptions?: Dictionary<any>;
@@ -415,6 +419,13 @@ export default class RoktManager {
415
419
  }
416
420
  }
417
421
 
422
+ // Read at click time, so selectors from a kit that attaches later apply to
423
+ // the next click.
424
+ public getCheckoutIntentSelectors(): ICheckoutIntentSelectors | undefined {
425
+ const kit = this.kit;
426
+ return kit ? kit.checkoutIntentSelectors : undefined;
427
+ }
428
+
418
429
  public discardCheckoutIntents(): void {
419
430
  this.pendingCheckoutIntents = [];
420
431
  }
@@ -484,11 +495,19 @@ export default class RoktManager {
484
495
  public async selectPlacements(options: IRoktSelectPlacementsOptions): Promise<IRoktSelection> {
485
496
  this.captureTiming?.(PerformanceMarkType.JointSdkSelectPlacements);
486
497
 
487
- // Queue if kit isn't ready OR if identity is in flight
488
- if (!this.isReady() || this.store?.identityCallInFlight) {
498
+ if (!this.isReady()) {
489
499
  return this.deferredCall<IRoktSelection>('selectPlacements', options);
490
500
  }
491
501
 
502
+ // Like the iOS and Android SDKs, this does not wait for an identity request, so the
503
+ // selection can go out for the user from before it. The log counts how often that happens.
504
+ if (this.store?.identityCallInFlight) {
505
+ this.loggingService?.log({
506
+ message: 'RoktManager: selectPlacements ran while an identity request was in flight',
507
+ code: ErrorCodes.SELECT_PLACEMENTS_IDENTITY_IN_FLIGHT,
508
+ });
509
+ }
510
+
492
511
  try {
493
512
  const { attributes } = options;
494
513
  const sandboxValue = attributes?.sandbox || null;
@@ -570,10 +589,6 @@ export default class RoktManager {
570
589
  );
571
590
  }
572
591
  this.setUserAttributes(result?.getUser?.(), userAttributes, newIdentities);
573
- // Drain any selectPlacements calls that were deferred while
574
- // identify was in-flight. By the time this callback fires,
575
- // identityCallInFlight has already been reset to false.
576
- this.processMessageQueue();
577
592
  }
578
593
  );
579
594
  } catch (error) {
@@ -896,7 +911,6 @@ export default class RoktManager {
896
911
  public onIdentityComplete(): void {
897
912
  if (this.isReady()) {
898
913
  this.currentUser = this.identityService.getCurrentUser();
899
- // Process any queued selectPlacements calls that were waiting for identity
900
914
  this.processMessageQueue();
901
915
  }
902
916
  }