@schibsted/pulse-sdk 8.0.0-rc.6 → 8.0.0-rc.7

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 (27) hide show
  1. package/dist/cjs/Tracker.js +89 -26
  2. package/dist/cjs/tracker-proxy/consts.js +1 -0
  3. package/dist/cjs/version.js +1 -1
  4. package/dist/cjs/visibility-observer/index.js +9 -7
  5. package/dist/cjs/wrapper/types/event.js +5 -0
  6. package/dist/ejs/Tracker.js +90 -27
  7. package/dist/ejs/tracker-proxy/consts.js +1 -0
  8. package/dist/ejs/version.js +1 -1
  9. package/dist/ejs/visibility-observer/index.js +9 -7
  10. package/dist/ejs/wrapper/types/event.js +4 -1
  11. package/dist/types/Tracker.d.ts +39 -3
  12. package/dist/types/tracker-proxy/consts.d.ts +1 -1
  13. package/dist/types/version.d.ts +1 -1
  14. package/dist/types/visibility-observer/index.d.ts +6 -1
  15. package/dist/types/wrapper/types/event.d.ts +3 -1
  16. package/node_modules/@schibsted/tpaas-event-builder/dist/cjs/builders.js +5 -1
  17. package/node_modules/@schibsted/tpaas-event-builder/dist/cjs/guards.js +7 -0
  18. package/node_modules/@schibsted/tpaas-event-builder/dist/cjs/index.js +3 -0
  19. package/node_modules/@schibsted/tpaas-event-builder/dist/ejs/builders.js +5 -2
  20. package/node_modules/@schibsted/tpaas-event-builder/dist/ejs/guards.js +4 -0
  21. package/node_modules/@schibsted/tpaas-event-builder/dist/ejs/index.js +1 -0
  22. package/node_modules/@schibsted/tpaas-event-builder/dist/index.d.ts +782 -164
  23. package/node_modules/@schibsted/tpaas-event-builder/package.json +1 -1
  24. package/node_modules/@schibsted/tpaas-schemas/dist/cjs/constants.js +66 -57
  25. package/node_modules/@schibsted/tpaas-schemas/dist/ejs/constants.js +64 -55
  26. package/node_modules/@schibsted/tpaas-schemas/dist/index.d.ts +836 -219
  27. package/package.json +5 -5
@@ -68,6 +68,7 @@ const version_1 = require("./version");
68
68
  const visibility_observer_1 = require("./visibility-observer");
69
69
  const warnOnce_1 = __importDefault(require("./warnOnce"));
70
70
  const event_builder_1 = require("./wrapper/engagement/ui-element/event-builder");
71
+ const types_1 = require("./wrapper/types");
71
72
  const page_from_page_view_1 = require("./wrapper/utils/page-from-page-view");
72
73
  const event_builder_2 = require("./wrapper/view/article/event-builder");
73
74
  const event_builder_3 = require("./wrapper/view/frontpage/event-builder");
@@ -157,6 +158,27 @@ function unsubscribe(eventName, callback) {
157
158
  * @class Tracker
158
159
  */
159
160
  class Tracker {
161
+ async getCurrentTpaasPage() {
162
+ const { id, type, url } = await this.currentPage;
163
+ return {
164
+ // remove the SDRN prefix, it's not expected for TPaaS
165
+ pageId: id.startsWith('sdrn:') ? id.split(':').slice(-1)[0] : id,
166
+ // make sure the pageType is valid for TPaaS
167
+ pageType: (0, tpaas_event_builder_1.isPageType)(type) ? type : 'Page',
168
+ url,
169
+ };
170
+ }
171
+ async getCurrentLegacyPage() {
172
+ const { id, type, url } = await this.currentPage;
173
+ // make sure the pageType is valid for legacy events
174
+ const validatedType = (0, types_1.isLegacyPageType)(type) ? type : 'Page';
175
+ return {
176
+ // add the SDRN prefix if it's not there, legacy events expect the full SDRN format
177
+ id: id.startsWith('sdrn:') ? id : `sdrn:${this.state.providerId}:${validatedType.toLowerCase()}:${id}`,
178
+ type: validatedType,
179
+ url,
180
+ };
181
+ }
160
182
  /**
161
183
  * > [!WARNING]
162
184
  * >
@@ -175,12 +197,18 @@ class Tracker {
175
197
  // the tracking of the same element with both old and new events at the same time
176
198
  this.legacyVisibilityObservers = new Map();
177
199
  this.visibilityObservers = new Map();
178
- this.legacyCurrentPage = {
179
- id: 'undefined-id',
200
+ /**
201
+ * @private
202
+ *
203
+ * This is used as the source of truth for the current page's information, which is updated based on the View Page-level event
204
+ * As a single source of truth for the current page, this needs to be compatible between legacy and TPaaS events, hence the need for a custom data type.
205
+ * Legacy events can provide an id with SDRN, while TPaaS events don't need it
206
+ */
207
+ this.currentPage = {
208
+ id: 'unknown',
180
209
  type: 'Page',
181
- url: typeof window !== 'undefined' ? window.location.href : '',
210
+ url: pulse_utils_1.isBrowser ? window.location.href : undefined,
182
211
  };
183
- this.currentPage = undefined;
184
212
  /**
185
213
  * Promise that resolves when a session ID becomes available
186
214
  * @private
@@ -192,7 +220,7 @@ class Tracker {
192
220
  */
193
221
  this.hasProxyListener = false;
194
222
  this.setPageDefaults = (pageViewEvent) => {
195
- this.legacyCurrentPage = (0, page_from_page_view_1.pageFromPageView)(pageViewEvent);
223
+ this.currentPage = (0, page_from_page_view_1.pageFromPageView)(pageViewEvent);
196
224
  if (pageViewEvent.origin) {
197
225
  const pageDefaults = { origin: pageViewEvent.origin };
198
226
  this.update(pageDefaults, true);
@@ -200,8 +228,8 @@ class Tracker {
200
228
  };
201
229
  this.setCurrentPage = (id, type) => {
202
230
  this.currentPage = {
203
- pageId: id,
204
- pageType: type,
231
+ id: id || 'unknown',
232
+ type: type || 'Page',
205
233
  url: pulse_utils_1.isBrowser ? window.location.href : undefined,
206
234
  };
207
235
  };
@@ -530,10 +558,11 @@ class Tracker {
530
558
  source: source || 'unknown',
531
559
  };
532
560
  }
533
- const [actor, consents, experiments] = await Promise.all([
561
+ const [actor, consents, experiments, currentPage] = await Promise.all([
534
562
  this.getActor(),
535
563
  this.getConsents(),
536
564
  this.adaptExperiments(this.builders.experiments),
565
+ this.getCurrentTpaasPage(),
537
566
  ]);
538
567
  const { environmentId, jwe } = (0, pulse_cis_sync_1.parsePulseCookies)();
539
568
  return {
@@ -544,7 +573,7 @@ class Tracker {
544
573
  consents: adaptConsentsToNewFormat(consents),
545
574
  providerId: this.state.providerId,
546
575
  pageviewId: this.state.pageViewId,
547
- currentPage: this.currentPage,
576
+ currentPage,
548
577
  experiments,
549
578
  environmentId,
550
579
  jwe,
@@ -621,7 +650,7 @@ class Tracker {
621
650
  const viewUIElementInputProperties = buildEventFromElement(element);
622
651
  this.trackLegacyViewUIElement(viewUIElementInputProperties);
623
652
  }));
624
- this.legacyVisibilityObservers.get(selector)?.observe(selector);
653
+ this.legacyVisibilityObservers.get(selector)?.observe(selector, {});
625
654
  }
626
655
  /**
627
656
  * The `addUIElementVisibilityTracking` function provides an interface for sending tracking events when specific UI elements become visible within the viewport.
@@ -653,7 +682,38 @@ class Tracker {
653
682
  const eventInput = buildEventFromElement(element);
654
683
  this.trackImpressionUIElement(eventInput);
655
684
  }));
656
- this.visibilityObservers.get(selector)?.observe(selector);
685
+ this.visibilityObservers.get(selector)?.observe(selector, {});
686
+ }
687
+ /**
688
+ * The `addElementVisibilityObserver` function provides an interface for executing arbitrary code when specific DOM Elements become visible within the viewport.
689
+ * It is built on the `VisibilityObserver` class, which efficiently detects visibility changes in DOM elements.
690
+ * When an element becomes at least 2/3 visible, a callback is triggered, with the Element that invoked the callback call as argument.
691
+ * It leverages the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) for viewport tracking
692
+ * and the [Mutation Observer API](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to monitor lazy-loaded elements.
693
+ *
694
+ * Example usage:
695
+ *
696
+ * ```ts
697
+ * tracker.addElementVisibilityObserver('[data-pulse-entity-id]', (element) => {
698
+ * const eventInput = parseElementIntoEventInput(element); // user-defined function
699
+ * tracker.trackImpressionTeaser(eventInput); // can be anything actually, not necessarily a Teaser tracking call
700
+ * });
701
+ * ```
702
+ * @param selector
703
+ * @param callback
704
+ * @param options {ObserverOptions}
705
+ * @param options.threshold - A number between 0 and 1 indicating how much of the element should be visible to trigger
706
+ * the callback. Default is 0.67 (2/3 of the element).
707
+ */
708
+ addElementVisibilityObserver(selector, callback, options = {}) {
709
+ if (this.visibilityObservers.has(selector)) {
710
+ console.warn(`Cannot add UI Element visibility tracking for selector "${selector}" as it is already being tracked.`);
711
+ return;
712
+ }
713
+ this.visibilityObservers.set(selector, new visibility_observer_1.VisibilityObserver((element) => {
714
+ callback(element);
715
+ }));
716
+ this.visibilityObservers.get(selector)?.observe(selector, options);
657
717
  }
658
718
  /**
659
719
  * The old trackViewArticle method from the Media Wrapper.
@@ -747,7 +807,7 @@ class Tracker {
747
807
  * @category Legacy Tracking
748
808
  */
749
809
  trackLegacyViewUIElement(inputProperties, trackOptions) {
750
- const viewUIElementEventData = (0, event_builder_8.buildLegacyViewUIElementEvent)(inputProperties, this.legacyCurrentPage);
810
+ const viewUIElementEventData = (0, event_builder_8.buildLegacyViewUIElementEvent)(inputProperties, this.getCurrentLegacyPage());
751
811
  return this.track('trackerEvent', viewUIElementEventData, trackOptions);
752
812
  }
753
813
  /**
@@ -758,7 +818,7 @@ class Tracker {
758
818
  * @category Legacy Tracking
759
819
  */
760
820
  trackLegacyClickUIElement(inputProperties, trackOptions) {
761
- const clickUIElementEventData = (0, event_builder_1.buildClickUIElementEvent)(inputProperties, this.legacyCurrentPage);
821
+ const clickUIElementEventData = (0, event_builder_1.buildClickUIElementEvent)(inputProperties, this.getCurrentLegacyPage());
762
822
  return this.track('trackerEvent', clickUIElementEventData, trackOptions);
763
823
  }
764
824
  /**
@@ -831,10 +891,9 @@ class Tracker {
831
891
  const result = this.sendTpaasEvent(event);
832
892
  if ((0, leaveTrackingTpaas_1.shouldEnableLeaveTracking)(options.leaveTracking)) {
833
893
  const leavePageCallback = async (leave) => {
834
- const { metrics, ...rest } = input; // metrics are not part of LeavePage schema, so we need to omit them
835
894
  await this.trackLeavePage({
836
895
  leave,
837
- ...rest,
896
+ ...input,
838
897
  });
839
898
  };
840
899
  (0, leaveTrackingTpaas_1.addTpaasLeaveTracking)({
@@ -905,12 +964,9 @@ class Tracker {
905
964
  const result = this.sendTpaasEvent(event);
906
965
  if ((0, leaveTrackingTpaas_1.shouldEnableLeaveTracking)(options.leaveTracking)) {
907
966
  const leaveArticleCallback = async (leave) => {
908
- // omit these properties from LeaveArticle because they're not part of the LeaveArticle schema
909
- const { authors, hotness, lifetime, sponsor, wordCount, ...rest } = input.object;
910
967
  await this.trackLeaveArticle({
911
968
  leave,
912
969
  ...input,
913
- object: rest,
914
970
  });
915
971
  };
916
972
  (0, leaveTrackingTpaas_1.addTpaasLeaveTracking)({
@@ -1020,6 +1076,15 @@ class Tracker {
1020
1076
  }
1021
1077
  return result;
1022
1078
  }
1079
+ /**
1080
+ * @param input
1081
+ * @category TPaaS Tracking
1082
+ */
1083
+ async trackImpressionAdSlot(input) {
1084
+ const dependencies = await this.prepareTpaasEvent();
1085
+ const event = (0, tpaas_event_builder_1.buildImpressionAdSlotEvent)(input, dependencies);
1086
+ return this.sendTpaasEvent(event);
1087
+ }
1023
1088
  /**
1024
1089
  * @param input
1025
1090
  * @category TPaaS Tracking
@@ -1078,10 +1143,9 @@ class Tracker {
1078
1143
  const result = this.sendTpaasEvent(event);
1079
1144
  if ((0, leaveTrackingTpaas_1.shouldEnableLeaveTracking)(options.leaveTracking)) {
1080
1145
  const leaveFrontpageCallback = async (leave) => {
1081
- const { metrics, ...rest } = input; // metrics are not part of LeaveFrontpage schema, so we need to omit them
1082
1146
  await this.trackLeaveFrontpage({
1083
1147
  leave,
1084
- ...rest,
1148
+ ...input,
1085
1149
  });
1086
1150
  };
1087
1151
  (0, leaveTrackingTpaas_1.addTpaasLeaveTracking)({
@@ -1134,6 +1198,10 @@ class Tracker {
1134
1198
  await this.trackLeaveListing({
1135
1199
  leave,
1136
1200
  ...input,
1201
+ object: {
1202
+ ...input.object,
1203
+ listingType: 'Tag,Story,Section,Search,Author,Other', // TODO: listingType is broken for this catalog. will be fixed in the next one
1204
+ },
1137
1205
  });
1138
1206
  };
1139
1207
  (0, leaveTrackingTpaas_1.addTpaasLeaveTracking)({
@@ -1157,12 +1225,9 @@ class Tracker {
1157
1225
  const result = this.sendTpaasEvent(event);
1158
1226
  if ((0, leaveTrackingTpaas_1.shouldEnableLeaveTracking)(options.leaveTracking)) {
1159
1227
  const leaveLockedArticleCallback = async (leave) => {
1160
- // omit these properties because they're not part of the LeaveLockedArticle schema
1161
- const { authors, hotness, lifetime, sponsor, wordCount, ...rest } = input.object;
1162
1228
  await this.trackLeaveLockedArticle({
1163
1229
  leave,
1164
1230
  ...input,
1165
- object: rest,
1166
1231
  });
1167
1232
  };
1168
1233
  (0, leaveTrackingTpaas_1.addTpaasLeaveTracking)({
@@ -1308,11 +1373,9 @@ class Tracker {
1308
1373
  const result = this.sendTpaasEvent(event);
1309
1374
  if ((0, leaveTrackingTpaas_1.shouldEnableLeaveTracking)(options.leaveTracking)) {
1310
1375
  const leaveWeatherCallback = async (leave) => {
1311
- // omit object and page from input because they're not part of the LeaveWeather schema
1312
- const { object, page, ...rest } = input;
1313
1376
  await this.trackLeaveWeather({
1314
1377
  leave,
1315
- ...rest,
1378
+ ...input,
1316
1379
  });
1317
1380
  };
1318
1381
  (0, leaveTrackingTpaas_1.addTpaasLeaveTracking)({
@@ -22,6 +22,7 @@ exports.TPAAS_ALLOWED_METHODS = [
22
22
  'trackEngagementVideoAd',
23
23
  'trackEngagementWidget',
24
24
  'trackCompletedHealthAction',
25
+ 'trackImpressionAdSlot',
25
26
  'trackImpressionForm',
26
27
  'trackImpressionHealthUIElement',
27
28
  'trackImpressionOffer',
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_VERSION = void 0;
4
4
  // This file is auto-generated by bin/generate-version.js. Do not edit manually.
5
- exports.SDK_VERSION = '8.0.0-rc.6';
5
+ exports.SDK_VERSION = '8.0.0-rc.7';
@@ -16,14 +16,16 @@ class VisibilityObserver {
16
16
  * Uses {@link https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver | MutationObserver} to monitor the DOM for dynamically loaded content.
17
17
  * Callback is invoked only once per element.
18
18
  * @param selector The CSS selector for elements to track.
19
+ * @param options {ObserverOptions} Optional configuration for the observer.
20
+ * @param options.threshold Which ratio triggers the Observer callback.
19
21
  * @throws Error if called more than once.
20
22
  */
21
- observe(selector) {
23
+ observe(selector, { threshold = 0.67 }) {
22
24
  if (this.selector) {
23
25
  throw new Error(`Cannot observe selector ${selector}. A visibility observer has already been set up for selector ${this.selector}.`);
24
26
  }
25
27
  this.selector = selector;
26
- this.setupObservers();
28
+ this.setupObservers({ threshold });
27
29
  // biome-ignore lint/suspicious/useIterableCallbackReturn: -
28
30
  this.elements().forEach((elementToTrack) => this.observeIntersection(elementToTrack));
29
31
  }
@@ -33,14 +35,14 @@ class VisibilityObserver {
33
35
  elements() {
34
36
  return Array.from(document.querySelectorAll(this.selector));
35
37
  }
36
- // Initialize both observers and start watching for DOM changes
37
- setupObservers() {
38
- this.setupIntersectionObserver();
38
+ // Initialise both observers and start watching for DOM changes
39
+ setupObservers({ threshold }) {
40
+ this.setupIntersectionObserver({ threshold });
39
41
  this.setupMutationObserver();
40
42
  this.observeMutations();
41
43
  }
42
44
  // Create observer to detect when elements become visible in the viewport
43
- setupIntersectionObserver() {
45
+ setupIntersectionObserver({ threshold }) {
44
46
  this.intersectionObserver = new IntersectionObserver((entries) => {
45
47
  entries.forEach((entry) => {
46
48
  if (entry.isIntersecting) {
@@ -49,7 +51,7 @@ class VisibilityObserver {
49
51
  this.intersectionObserver.unobserve(node);
50
52
  }
51
53
  });
52
- }, { threshold: 2 / 3 });
54
+ }, { threshold });
53
55
  }
54
56
  // Create observer to detect DOM changes that may add/remove tracked elements
55
57
  setupMutationObserver() {
@@ -1,2 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isLegacyPageType = isLegacyPageType;
4
+ const LEGACY_PAGE_TYPE_VALUES = ['Article', 'Listing', 'Frontpage', 'SalesPoster', 'Page', 'LoginPoster'];
5
+ function isLegacyPageType(value) {
6
+ return LEGACY_PAGE_TYPE_VALUES.includes(value);
7
+ }
@@ -1,6 +1,6 @@
1
1
  import { parsePulseCookies } from '@schibsted/pulse-cis-sync';
2
2
  import { evaluateAndFlatten, isBrowser, isFunction, isPromise, isString, pulseMerge, throttle, } from '@schibsted/pulse-utils';
3
- import { buildAnonymousViewUIElementEvent, buildCompletedHealthActionEvent, buildEngagementAudioEvent, buildEngagementFormEvent, buildEngagementHealthUIElementEvent, buildEngagementOfferEvent, buildEngagementTeaserEvent, buildEngagementUIElementEvent, buildEngagementVideoAdEvent, buildEngagementVideoEvent, buildEngagementWidgetEvent, buildImpressionFormEvent, buildImpressionHealthUIElementEvent, buildImpressionOfferEvent, buildImpressionPlayerEvent, buildImpressionTeaserEvent, buildImpressionUIElementEvent, buildImpressionWidgetEvent, buildLeaveArticleEvent, buildLeaveAudioPageEvent, buildLeaveErrorEvent, buildLeaveFrontpageEvent, buildLeaveLandingpageEvent, buildLeaveListingEvent, buildLeaveLockedArticleEvent, buildLeaveLockedAudioPageEvent, buildLeaveLockedVideoPageEvent, buildLeavePageEvent, buildLeaveSportsPageEvent, buildLeaveVideoPageEvent, buildLeaveWeatherEvent, buildViewArticleEvent, buildViewAudioPageEvent, buildViewErrorEvent, buildViewFrontpageEvent, buildViewHealthPageEvent, buildViewLandingpageEvent, buildViewListingEvent, buildViewLockedArticleEvent, buildViewLockedAudioPageEvent, buildViewLockedVideoPageEvent, buildViewPageEvent, buildViewSportsPageEvent, buildViewTitleEvent, buildViewVideoPageEvent, buildViewWeatherEvent, getCommonBuildersOutput, } from '@schibsted/tpaas-event-builder';
3
+ import { buildAnonymousViewUIElementEvent, buildCompletedHealthActionEvent, buildEngagementAudioEvent, buildEngagementFormEvent, buildEngagementHealthUIElementEvent, buildEngagementOfferEvent, buildEngagementTeaserEvent, buildEngagementUIElementEvent, buildEngagementVideoAdEvent, buildEngagementVideoEvent, buildEngagementWidgetEvent, buildImpressionAdSlotEvent, buildImpressionFormEvent, buildImpressionHealthUIElementEvent, buildImpressionOfferEvent, buildImpressionPlayerEvent, buildImpressionTeaserEvent, buildImpressionUIElementEvent, buildImpressionWidgetEvent, buildLeaveArticleEvent, buildLeaveAudioPageEvent, buildLeaveErrorEvent, buildLeaveFrontpageEvent, buildLeaveLandingpageEvent, buildLeaveListingEvent, buildLeaveLockedArticleEvent, buildLeaveLockedAudioPageEvent, buildLeaveLockedVideoPageEvent, buildLeavePageEvent, buildLeaveSportsPageEvent, buildLeaveVideoPageEvent, buildLeaveWeatherEvent, buildViewArticleEvent, buildViewAudioPageEvent, buildViewErrorEvent, buildViewFrontpageEvent, buildViewHealthPageEvent, buildViewLandingpageEvent, buildViewListingEvent, buildViewLockedArticleEvent, buildViewLockedAudioPageEvent, buildViewLockedVideoPageEvent, buildViewPageEvent, buildViewSportsPageEvent, buildViewTitleEvent, buildViewVideoPageEvent, buildViewWeatherEvent, getCommonBuildersOutput, isPageType, } from '@schibsted/tpaas-event-builder';
4
4
  import { LeaveArticle, LeaveAudioPage, LeaveError, LeaveFrontpage, LeaveLandingpage, LeaveListing, LeaveLockedArticle, LeaveLockedAudioPage, LeaveLockedVideoPage, LeavePage, LeaveSportsPage, LeaveVideoPage, LeaveWeather, ViewArticle, ViewAudioPage, ViewError, ViewFrontpage, ViewHealthPage, ViewLandingpage, ViewListing, ViewLockedArticle, ViewLockedAudioPage, ViewLockedVideoPage, ViewPage, ViewSportsPage, ViewTitle, ViewVideoPage, ViewWeather, } from '@schibsted/tpaas-schemas';
5
5
  import logger from 'loglevel';
6
6
  import { v4 } from 'uuid';
@@ -25,6 +25,7 @@ import { SDK_VERSION } from './version';
25
25
  import { VisibilityObserver } from './visibility-observer';
26
26
  import warnOnce from './warnOnce';
27
27
  import { buildClickUIElementEvent } from './wrapper/engagement/ui-element/event-builder';
28
+ import { isLegacyPageType } from './wrapper/types';
28
29
  import { pageFromPageView } from './wrapper/utils/page-from-page-view';
29
30
  import { buildLegacyViewArticleEvent } from './wrapper/view/article/event-builder';
30
31
  import { buildLegacyViewFrontpageEvent } from './wrapper/view/frontpage/event-builder';
@@ -114,6 +115,27 @@ export function unsubscribe(eventName, callback) {
114
115
  * @class Tracker
115
116
  */
116
117
  export default class Tracker {
118
+ async getCurrentTpaasPage() {
119
+ const { id, type, url } = await this.currentPage;
120
+ return {
121
+ // remove the SDRN prefix, it's not expected for TPaaS
122
+ pageId: id.startsWith('sdrn:') ? id.split(':').slice(-1)[0] : id,
123
+ // make sure the pageType is valid for TPaaS
124
+ pageType: isPageType(type) ? type : 'Page',
125
+ url,
126
+ };
127
+ }
128
+ async getCurrentLegacyPage() {
129
+ const { id, type, url } = await this.currentPage;
130
+ // make sure the pageType is valid for legacy events
131
+ const validatedType = isLegacyPageType(type) ? type : 'Page';
132
+ return {
133
+ // add the SDRN prefix if it's not there, legacy events expect the full SDRN format
134
+ id: id.startsWith('sdrn:') ? id : `sdrn:${this.state.providerId}:${validatedType.toLowerCase()}:${id}`,
135
+ type: validatedType,
136
+ url,
137
+ };
138
+ }
117
139
  /**
118
140
  * > [!WARNING]
119
141
  * >
@@ -132,12 +154,18 @@ export default class Tracker {
132
154
  // the tracking of the same element with both old and new events at the same time
133
155
  this.legacyVisibilityObservers = new Map();
134
156
  this.visibilityObservers = new Map();
135
- this.legacyCurrentPage = {
136
- id: 'undefined-id',
157
+ /**
158
+ * @private
159
+ *
160
+ * This is used as the source of truth for the current page's information, which is updated based on the View Page-level event
161
+ * As a single source of truth for the current page, this needs to be compatible between legacy and TPaaS events, hence the need for a custom data type.
162
+ * Legacy events can provide an id with SDRN, while TPaaS events don't need it
163
+ */
164
+ this.currentPage = {
165
+ id: 'unknown',
137
166
  type: 'Page',
138
- url: typeof window !== 'undefined' ? window.location.href : '',
167
+ url: isBrowser ? window.location.href : undefined,
139
168
  };
140
- this.currentPage = undefined;
141
169
  /**
142
170
  * Promise that resolves when a session ID becomes available
143
171
  * @private
@@ -149,7 +177,7 @@ export default class Tracker {
149
177
  */
150
178
  this.hasProxyListener = false;
151
179
  this.setPageDefaults = (pageViewEvent) => {
152
- this.legacyCurrentPage = pageFromPageView(pageViewEvent);
180
+ this.currentPage = pageFromPageView(pageViewEvent);
153
181
  if (pageViewEvent.origin) {
154
182
  const pageDefaults = { origin: pageViewEvent.origin };
155
183
  this.update(pageDefaults, true);
@@ -157,8 +185,8 @@ export default class Tracker {
157
185
  };
158
186
  this.setCurrentPage = (id, type) => {
159
187
  this.currentPage = {
160
- pageId: id,
161
- pageType: type,
188
+ id: id || 'unknown',
189
+ type: type || 'Page',
162
190
  url: isBrowser ? window.location.href : undefined,
163
191
  };
164
192
  };
@@ -487,10 +515,11 @@ export default class Tracker {
487
515
  source: source || 'unknown',
488
516
  };
489
517
  }
490
- const [actor, consents, experiments] = await Promise.all([
518
+ const [actor, consents, experiments, currentPage] = await Promise.all([
491
519
  this.getActor(),
492
520
  this.getConsents(),
493
521
  this.adaptExperiments(this.builders.experiments),
522
+ this.getCurrentTpaasPage(),
494
523
  ]);
495
524
  const { environmentId, jwe } = parsePulseCookies();
496
525
  return {
@@ -501,7 +530,7 @@ export default class Tracker {
501
530
  consents: adaptConsentsToNewFormat(consents),
502
531
  providerId: this.state.providerId,
503
532
  pageviewId: this.state.pageViewId,
504
- currentPage: this.currentPage,
533
+ currentPage,
505
534
  experiments,
506
535
  environmentId,
507
536
  jwe,
@@ -578,7 +607,7 @@ export default class Tracker {
578
607
  const viewUIElementInputProperties = buildEventFromElement(element);
579
608
  this.trackLegacyViewUIElement(viewUIElementInputProperties);
580
609
  }));
581
- this.legacyVisibilityObservers.get(selector)?.observe(selector);
610
+ this.legacyVisibilityObservers.get(selector)?.observe(selector, {});
582
611
  }
583
612
  /**
584
613
  * The `addUIElementVisibilityTracking` function provides an interface for sending tracking events when specific UI elements become visible within the viewport.
@@ -610,7 +639,38 @@ export default class Tracker {
610
639
  const eventInput = buildEventFromElement(element);
611
640
  this.trackImpressionUIElement(eventInput);
612
641
  }));
613
- this.visibilityObservers.get(selector)?.observe(selector);
642
+ this.visibilityObservers.get(selector)?.observe(selector, {});
643
+ }
644
+ /**
645
+ * The `addElementVisibilityObserver` function provides an interface for executing arbitrary code when specific DOM Elements become visible within the viewport.
646
+ * It is built on the `VisibilityObserver` class, which efficiently detects visibility changes in DOM elements.
647
+ * When an element becomes at least 2/3 visible, a callback is triggered, with the Element that invoked the callback call as argument.
648
+ * It leverages the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) for viewport tracking
649
+ * and the [Mutation Observer API](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to monitor lazy-loaded elements.
650
+ *
651
+ * Example usage:
652
+ *
653
+ * ```ts
654
+ * tracker.addElementVisibilityObserver('[data-pulse-entity-id]', (element) => {
655
+ * const eventInput = parseElementIntoEventInput(element); // user-defined function
656
+ * tracker.trackImpressionTeaser(eventInput); // can be anything actually, not necessarily a Teaser tracking call
657
+ * });
658
+ * ```
659
+ * @param selector
660
+ * @param callback
661
+ * @param options {ObserverOptions}
662
+ * @param options.threshold - A number between 0 and 1 indicating how much of the element should be visible to trigger
663
+ * the callback. Default is 0.67 (2/3 of the element).
664
+ */
665
+ addElementVisibilityObserver(selector, callback, options = {}) {
666
+ if (this.visibilityObservers.has(selector)) {
667
+ console.warn(`Cannot add UI Element visibility tracking for selector "${selector}" as it is already being tracked.`);
668
+ return;
669
+ }
670
+ this.visibilityObservers.set(selector, new VisibilityObserver((element) => {
671
+ callback(element);
672
+ }));
673
+ this.visibilityObservers.get(selector)?.observe(selector, options);
614
674
  }
615
675
  /**
616
676
  * The old trackViewArticle method from the Media Wrapper.
@@ -704,7 +764,7 @@ export default class Tracker {
704
764
  * @category Legacy Tracking
705
765
  */
706
766
  trackLegacyViewUIElement(inputProperties, trackOptions) {
707
- const viewUIElementEventData = buildLegacyViewUIElementEvent(inputProperties, this.legacyCurrentPage);
767
+ const viewUIElementEventData = buildLegacyViewUIElementEvent(inputProperties, this.getCurrentLegacyPage());
708
768
  return this.track('trackerEvent', viewUIElementEventData, trackOptions);
709
769
  }
710
770
  /**
@@ -715,7 +775,7 @@ export default class Tracker {
715
775
  * @category Legacy Tracking
716
776
  */
717
777
  trackLegacyClickUIElement(inputProperties, trackOptions) {
718
- const clickUIElementEventData = buildClickUIElementEvent(inputProperties, this.legacyCurrentPage);
778
+ const clickUIElementEventData = buildClickUIElementEvent(inputProperties, this.getCurrentLegacyPage());
719
779
  return this.track('trackerEvent', clickUIElementEventData, trackOptions);
720
780
  }
721
781
  /**
@@ -788,10 +848,9 @@ export default class Tracker {
788
848
  const result = this.sendTpaasEvent(event);
789
849
  if (shouldEnableLeaveTracking(options.leaveTracking)) {
790
850
  const leavePageCallback = async (leave) => {
791
- const { metrics, ...rest } = input; // metrics are not part of LeavePage schema, so we need to omit them
792
851
  await this.trackLeavePage({
793
852
  leave,
794
- ...rest,
853
+ ...input,
795
854
  });
796
855
  };
797
856
  addTpaasLeaveTracking({
@@ -862,12 +921,9 @@ export default class Tracker {
862
921
  const result = this.sendTpaasEvent(event);
863
922
  if (shouldEnableLeaveTracking(options.leaveTracking)) {
864
923
  const leaveArticleCallback = async (leave) => {
865
- // omit these properties from LeaveArticle because they're not part of the LeaveArticle schema
866
- const { authors, hotness, lifetime, sponsor, wordCount, ...rest } = input.object;
867
924
  await this.trackLeaveArticle({
868
925
  leave,
869
926
  ...input,
870
- object: rest,
871
927
  });
872
928
  };
873
929
  addTpaasLeaveTracking({
@@ -977,6 +1033,15 @@ export default class Tracker {
977
1033
  }
978
1034
  return result;
979
1035
  }
1036
+ /**
1037
+ * @param input
1038
+ * @category TPaaS Tracking
1039
+ */
1040
+ async trackImpressionAdSlot(input) {
1041
+ const dependencies = await this.prepareTpaasEvent();
1042
+ const event = buildImpressionAdSlotEvent(input, dependencies);
1043
+ return this.sendTpaasEvent(event);
1044
+ }
980
1045
  /**
981
1046
  * @param input
982
1047
  * @category TPaaS Tracking
@@ -1035,10 +1100,9 @@ export default class Tracker {
1035
1100
  const result = this.sendTpaasEvent(event);
1036
1101
  if (shouldEnableLeaveTracking(options.leaveTracking)) {
1037
1102
  const leaveFrontpageCallback = async (leave) => {
1038
- const { metrics, ...rest } = input; // metrics are not part of LeaveFrontpage schema, so we need to omit them
1039
1103
  await this.trackLeaveFrontpage({
1040
1104
  leave,
1041
- ...rest,
1105
+ ...input,
1042
1106
  });
1043
1107
  };
1044
1108
  addTpaasLeaveTracking({
@@ -1091,6 +1155,10 @@ export default class Tracker {
1091
1155
  await this.trackLeaveListing({
1092
1156
  leave,
1093
1157
  ...input,
1158
+ object: {
1159
+ ...input.object,
1160
+ listingType: 'Tag,Story,Section,Search,Author,Other', // TODO: listingType is broken for this catalog. will be fixed in the next one
1161
+ },
1094
1162
  });
1095
1163
  };
1096
1164
  addTpaasLeaveTracking({
@@ -1114,12 +1182,9 @@ export default class Tracker {
1114
1182
  const result = this.sendTpaasEvent(event);
1115
1183
  if (shouldEnableLeaveTracking(options.leaveTracking)) {
1116
1184
  const leaveLockedArticleCallback = async (leave) => {
1117
- // omit these properties because they're not part of the LeaveLockedArticle schema
1118
- const { authors, hotness, lifetime, sponsor, wordCount, ...rest } = input.object;
1119
1185
  await this.trackLeaveLockedArticle({
1120
1186
  leave,
1121
1187
  ...input,
1122
- object: rest,
1123
1188
  });
1124
1189
  };
1125
1190
  addTpaasLeaveTracking({
@@ -1265,11 +1330,9 @@ export default class Tracker {
1265
1330
  const result = this.sendTpaasEvent(event);
1266
1331
  if (shouldEnableLeaveTracking(options.leaveTracking)) {
1267
1332
  const leaveWeatherCallback = async (leave) => {
1268
- // omit object and page from input because they're not part of the LeaveWeather schema
1269
- const { object, page, ...rest } = input;
1270
1333
  await this.trackLeaveWeather({
1271
1334
  leave,
1272
- ...rest,
1335
+ ...input,
1273
1336
  });
1274
1337
  };
1275
1338
  addTpaasLeaveTracking({
@@ -18,6 +18,7 @@ export const TPAAS_ALLOWED_METHODS = [
18
18
  'trackEngagementVideoAd',
19
19
  'trackEngagementWidget',
20
20
  'trackCompletedHealthAction',
21
+ 'trackImpressionAdSlot',
21
22
  'trackImpressionForm',
22
23
  'trackImpressionHealthUIElement',
23
24
  'trackImpressionOffer',
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated by bin/generate-version.js. Do not edit manually.
2
- export const SDK_VERSION = '8.0.0-rc.6';
2
+ export const SDK_VERSION = '8.0.0-rc.7';
@@ -13,14 +13,16 @@ export class VisibilityObserver {
13
13
  * Uses {@link https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver | MutationObserver} to monitor the DOM for dynamically loaded content.
14
14
  * Callback is invoked only once per element.
15
15
  * @param selector The CSS selector for elements to track.
16
+ * @param options {ObserverOptions} Optional configuration for the observer.
17
+ * @param options.threshold Which ratio triggers the Observer callback.
16
18
  * @throws Error if called more than once.
17
19
  */
18
- observe(selector) {
20
+ observe(selector, { threshold = 0.67 }) {
19
21
  if (this.selector) {
20
22
  throw new Error(`Cannot observe selector ${selector}. A visibility observer has already been set up for selector ${this.selector}.`);
21
23
  }
22
24
  this.selector = selector;
23
- this.setupObservers();
25
+ this.setupObservers({ threshold });
24
26
  // biome-ignore lint/suspicious/useIterableCallbackReturn: -
25
27
  this.elements().forEach((elementToTrack) => this.observeIntersection(elementToTrack));
26
28
  }
@@ -30,14 +32,14 @@ export class VisibilityObserver {
30
32
  elements() {
31
33
  return Array.from(document.querySelectorAll(this.selector));
32
34
  }
33
- // Initialize both observers and start watching for DOM changes
34
- setupObservers() {
35
- this.setupIntersectionObserver();
35
+ // Initialise both observers and start watching for DOM changes
36
+ setupObservers({ threshold }) {
37
+ this.setupIntersectionObserver({ threshold });
36
38
  this.setupMutationObserver();
37
39
  this.observeMutations();
38
40
  }
39
41
  // Create observer to detect when elements become visible in the viewport
40
- setupIntersectionObserver() {
42
+ setupIntersectionObserver({ threshold }) {
41
43
  this.intersectionObserver = new IntersectionObserver((entries) => {
42
44
  entries.forEach((entry) => {
43
45
  if (entry.isIntersecting) {
@@ -46,7 +48,7 @@ export class VisibilityObserver {
46
48
  this.intersectionObserver.unobserve(node);
47
49
  }
48
50
  });
49
- }, { threshold: 2 / 3 });
51
+ }, { threshold });
50
52
  }
51
53
  // Create observer to detect DOM changes that may add/remove tracked elements
52
54
  setupMutationObserver() {
@@ -1 +1,4 @@
1
- export {};
1
+ const LEGACY_PAGE_TYPE_VALUES = ['Article', 'Listing', 'Frontpage', 'SalesPoster', 'Page', 'LoginPoster'];
2
+ export function isLegacyPageType(value) {
3
+ return LEGACY_PAGE_TYPE_VALUES.includes(value);
4
+ }