@guardian/ophan-tracker-js 2.1.0-next.1 → 2.1.0-next.2

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,24 +1,19 @@
1
- // Generated by CoffeeScript 2.7.0
2
1
  import abd from './vendor/adBlockDetectionLib.js';
3
2
 
4
3
  export default {
5
- run: function () {
6
- return new Promise(function (resolve, reject) {
7
- var e;
4
+ run() {
5
+ return new Promise((resolve, reject) => {
8
6
  if (abd.init) {
9
7
  try {
10
- return abd.init({
11
- complete: function (v) {
12
- return resolve(v);
13
- },
8
+ abd.init({
9
+ complete: resolve,
14
10
  });
15
11
  } catch (error) {
16
- e = error;
17
- return reject(e);
12
+ reject(error);
18
13
  }
19
14
  } else {
20
- return reject(new Error('Failed to initialise adblock detection'));
15
+ reject(new Error('Failed to initialise adblock detection'));
21
16
  }
22
17
  });
23
18
  },
24
- };
19
+ };
@@ -1,21 +1,4 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
- var ATTENTIONDECAY,
3
- EVENTS,
4
- REPORTINGINTERVAL,
5
- cancelDecayTimer,
6
- decayTimerId,
7
- incrementTotalAttentionTimeByUnrecordedAmount,
8
- initAttention,
9
- initComponent,
10
- makeActive,
11
- makeInactive,
12
- reportedTotalAttentionMs,
13
- reporter,
14
- setEventEmitter,
15
- totalAttentionMs,
16
- unrecordedAttentionStarted,
17
- videoPlaying;
18
-
19
2
  import transmit from './transmit.js';
20
3
 
21
4
  import components from './components.js';
@@ -32,10 +15,10 @@ import components from './components.js';
32
15
  // a page where the above conditions have held true."
33
16
 
34
17
  // our definition of "within a certain timeout" above:
35
- ATTENTIONDECAY = 5000;
18
+ const ATTENTIONDECAY = 5000;
36
19
 
37
20
  // our definition of "the user has interacted with the page"
38
- EVENTS = [
21
+ const EVENTS = [
39
22
  'focus',
40
23
  'click',
41
24
  'scroll',
@@ -49,90 +32,82 @@ EVENTS = [
49
32
  'keydown',
50
33
  ];
51
34
 
52
- REPORTINGINTERVAL = 10000;
35
+ const REPORTINGINTERVAL = 10000;
53
36
 
54
37
  // total attention time so far on this page
55
- totalAttentionMs = 0;
38
+ let totalAttentionMs = 0;
56
39
 
57
40
  // the time elapsed since the time origin (https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin)
58
41
  // when the most recent period of attention (that is, attention
59
42
  // which we haven't yet added to totalAttentionMs) began.
60
43
  // null if we don't have attention.
61
- unrecordedAttentionStarted = null;
44
+ let unrecordedAttentionStarted = null;
62
45
 
63
46
  // the attention time we last reported to ophan
64
- reportedTotalAttentionMs = null; // setting null here forces a first report of attention time, even if it's only 0ms
47
+ let reportedTotalAttentionMs = null; // setting null here forces a first report of attention time, even if it's only 0ms
65
48
 
66
49
  // the decay timer if we've set one
67
- decayTimerId = null;
50
+ let decayTimerId = null;
68
51
 
69
52
  // is video currently playing?
70
- videoPlaying = false;
53
+ let videoPlaying = false;
71
54
 
72
- cancelDecayTimer = function () {
55
+ const cancelDecayTimer = () => {
73
56
  if (decayTimerId != null) {
74
57
  window.clearTimeout(decayTimerId);
75
58
  }
76
- return (decayTimerId = null);
59
+ decayTimerId = null;
77
60
  };
78
61
 
79
- makeActive = function () {
80
- // mark when the latest period of attention began,
81
- // if we're not already in an attention period
62
+ const makeActive = () => {
82
63
  if (unrecordedAttentionStarted == null) {
83
64
  unrecordedAttentionStarted = performance.now();
84
65
  }
85
66
  components.startMonitoring();
86
- // start decay timer
87
67
  cancelDecayTimer();
88
- return (decayTimerId = window.setTimeout(function () {
68
+ decayTimerId = window.setTimeout(() => {
89
69
  if (!videoPlaying) {
90
- return makeInactive();
70
+ makeInactive();
91
71
  }
92
- }, ATTENTIONDECAY));
72
+ }, ATTENTIONDECAY);
93
73
  };
94
74
 
95
- incrementTotalAttentionTimeByUnrecordedAmount = function () {
96
- var now, unrecordedMs;
75
+ const incrementTotalAttentionTimeByUnrecordedAmount = () => {
97
76
  if (unrecordedAttentionStarted != null) {
98
- now = performance.now();
99
- // the Math.min here is to deal with occasions where we don't get an event indicating that the user
100
- // has become inactive - this can happen e.g. due to users suspending their phone. Never send
101
- // crazy big values!
102
- unrecordedMs = Math.min(
77
+ const now = performance.now();
78
+ const unrecordedMs = Math.min(
103
79
  now - unrecordedAttentionStarted,
104
80
  REPORTINGINTERVAL,
105
81
  );
106
82
  totalAttentionMs += unrecordedMs;
107
- return (unrecordedAttentionStarted = now);
83
+ unrecordedAttentionStarted = now;
108
84
  }
109
85
  };
110
86
 
111
- makeInactive = function () {
87
+ const makeInactive = () => {
112
88
  cancelDecayTimer();
113
89
  components.stopMonitoring();
114
90
  incrementTotalAttentionTimeByUnrecordedAmount();
115
- return (unrecordedAttentionStarted = null);
91
+ unrecordedAttentionStarted = null;
116
92
  };
117
93
 
118
- reporter = function () {
119
- var componentAttentionTimes, report;
94
+ const reporter = () => {
120
95
  incrementTotalAttentionTimeByUnrecordedAmount();
121
96
  if (totalAttentionMs !== reportedTotalAttentionMs) {
122
- report = {
97
+ const report = {
123
98
  attentionMs: Math.round(totalAttentionMs),
124
99
  };
125
- componentAttentionTimes = components.getAttentionTimes();
100
+ const componentAttentionTimes = components.getAttentionTimes();
126
101
  if (Object.keys(componentAttentionTimes).length) {
127
102
  report.componentAttentionMs = componentAttentionTimes;
128
103
  }
129
104
  transmit.sendMore(report);
130
- return (reportedTotalAttentionMs = totalAttentionMs);
105
+ reportedTotalAttentionMs = totalAttentionMs;
131
106
  }
132
107
  };
133
108
 
134
- initComponent = function (name, el, visibilityThreshold = 0.5) {
135
- return components.registerComponent(
109
+ const initComponent = (name, el, visibilityThreshold = 0.5) => {
110
+ components.registerComponent(
136
111
  name,
137
112
  el,
138
113
  visibilityThreshold,
@@ -140,48 +115,46 @@ initComponent = function (name, el, visibilityThreshold = 0.5) {
140
115
  );
141
116
  };
142
117
 
143
- initAttention = function (visibility) {
144
- var event, i, len;
145
- for (i = 0, len = EVENTS.length; i < len; i++) {
146
- event = EVENTS[i];
147
- // user interaction tracking
118
+ const initAttention = (visibility) => {
119
+ EVENTS.forEach((event) => {
148
120
  window.addEventListener(event, makeActive);
149
- }
150
- // focus tracking
121
+ });
122
+
151
123
  document.addEventListener(
152
124
  visibility.changeEvent,
153
- function () {
125
+ () => {
154
126
  if (visibility.state() === 'visible') {
155
- return makeActive();
127
+ makeActive();
156
128
  } else {
157
- return makeInactive();
129
+ makeInactive();
158
130
  }
159
131
  },
160
132
  false,
161
133
  );
162
- // video tracking
163
- document.addEventListener('videoPlaying', function () {
134
+
135
+ document.addEventListener('videoPlaying', () => {
164
136
  videoPlaying = true;
165
- return makeActive();
137
+ makeActive();
166
138
  });
167
- document.addEventListener('videoEnded', function () {
139
+ document.addEventListener('videoEnded', () => {
168
140
  videoPlaying = false;
169
- return makeInactive();
141
+ makeInactive();
170
142
  });
171
- document.addEventListener('videoPause', function () {
143
+ document.addEventListener('videoPause', () => {
172
144
  videoPlaying = false;
173
- return makeInactive();
145
+ makeInactive();
174
146
  });
147
+
175
148
  window.setTimeout(reporter, 100);
176
- return window.setInterval(reporter, REPORTINGINTERVAL);
149
+ window.setInterval(reporter, REPORTINGINTERVAL);
177
150
  };
178
151
 
179
- setEventEmitter = function (emitter) {
180
- return components.setEventEmitter(emitter);
152
+ const setEventEmitter = (emitter) => {
153
+ components.setEventEmitter(emitter);
181
154
  };
182
155
 
183
156
  export default {
184
157
  init: initAttention,
185
158
  initComponent: initComponent,
186
159
  setEventEmitter: setEventEmitter,
187
- };
160
+ };
@@ -1,40 +1,25 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
- var ref, ref1, start;
3
-
4
2
  import transmit from './transmit.js';
5
-
6
3
  import perf from './perf.js';
7
4
 
8
- start =
9
- (ref =
10
- perf != null
11
- ? (ref1 = perf.timing) != null
12
- ? ref1.domComplete
13
- : void 0
14
- : void 0) != null
15
- ? ref
16
- : performance.now();
5
+ const start = perf?.timing?.domComplete ?? performance.now();
17
6
 
18
7
  // DFP ads
19
8
  if (typeof googletag !== 'undefined' && googletag !== null) {
20
- googletag.cmd.push(function () {
21
- return googletag
22
- .pubads()
23
- .addEventListener('slotRenderEnded', function (event) {
24
- var slotId;
25
- slotId = event.slot.getSlotId().getDomId();
26
- return transmit.sendMore({
27
- ads: [
28
- {
29
- slot: slotId,
30
- campaignId: event.isEmpty ? '__empty__' : event.lineItemId,
31
- creativeId: event.creativeId,
32
- // overall time to render an ad
33
- timeToRenderEnded: Math.round(performance.now() - start),
34
- adServer: 'DFP',
35
- },
36
- ],
37
- });
9
+ googletag.cmd.push(() => {
10
+ googletag.pubads().addEventListener('slotRenderEnded', (event) => {
11
+ const slotId = event.slot.getSlotId().getDomId();
12
+ transmit.sendMore({
13
+ ads: [
14
+ {
15
+ slot: slotId,
16
+ campaignId: event.isEmpty ? '__empty__' : event.lineItemId,
17
+ creativeId: event.creativeId,
18
+ timeToRenderEnded: Math.round(performance.now() - start),
19
+ adServer: 'DFP',
20
+ },
21
+ ],
38
22
  });
23
+ });
39
24
  });
40
- }
25
+ }
@@ -1,81 +1,54 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var getContainingComponent, getDataLinkNames, validAncestorAnchorElement;
3
-
4
1
  import ophan from './core.js';
5
2
 
6
3
  import transmit from './transmit.js';
7
4
 
8
- validAncestorAnchorElement = function (el) {
9
- var elementType, ref;
10
- elementType =
11
- el != null
12
- ? (ref = el.nodeName) != null
13
- ? ref.toLowerCase()
14
- : void 0
15
- : void 0;
16
- if (elementType === 'a') {
17
- return el;
18
- } else if (elementType == null || elementType === 'body') {
19
- return null;
20
- } else {
21
- return validAncestorAnchorElement(el.parentNode);
22
- }
5
+ /**
6
+ * Finds the closest ancestor anchor (`<a>`) element of the given element.
7
+ *
8
+ * @param {Element} el - The element to start searching from.
9
+ * @returns {Element|null} The closest ancestor anchor element or null if none found.
10
+ */
11
+ export const validAncestorAnchorElement = (el) => {
12
+ if (!el || el.nodeName?.toLowerCase() === 'body') return null;
13
+ return el.nodeName.toLowerCase() === 'a'
14
+ ? el
15
+ : validAncestorAnchorElement(el.parentNode);
23
16
  };
24
17
 
25
- getContainingComponent = function (el) {
26
- var elementType, ref;
27
- elementType =
28
- el != null
29
- ? (ref = el.nodeName) != null
30
- ? ref.toLowerCase()
31
- : void 0
32
- : void 0;
33
- if (elementType == null || elementType === 'body') {
34
- return null;
35
- } else {
36
- return (
37
- (typeof el.getAttribute === 'function'
38
- ? el.getAttribute('data-component')
39
- : void 0) || getContainingComponent(el.parentNode)
40
- );
41
- }
18
+ /**
19
+ * Retrieves the value of the `data-component` attribute from the closest ancestor.
20
+ *
21
+ * @param {Element} el - The element to start searching from.
22
+ * @returns {string|null} The `data-component` attribute value or null if not found.
23
+ */
24
+ export const getContainingComponent = (el) => {
25
+ if (!el || el.nodeName?.toLowerCase() === 'body') return null;
26
+ return (
27
+ el.getAttribute('data-component') || getContainingComponent(el.parentNode)
28
+ );
42
29
  };
43
30
 
44
- getDataLinkNames = function (el, dataLinkNames = []) {
45
- var isBody, isDocument, walkTree;
46
- isBody = function (elem) {
47
- var ref;
48
- return (
49
- (elem != null
50
- ? (ref = elem.nodeName) != null
51
- ? ref.toLowerCase()
52
- : void 0
53
- : void 0) === document.body.nodeName.toLowerCase()
54
- );
55
- };
56
- isDocument = function (elem) {
57
- return elem === document;
58
- };
59
- walkTree = function (elem, dataLinkNames) {
60
- var dataLinkName;
61
- if (elem != null && !isBody(elem) && !isDocument(elem)) {
62
- dataLinkName = elem.getAttribute('data-link-name');
63
- if (dataLinkName != null) {
64
- dataLinkNames.push(dataLinkName);
65
- }
66
- return walkTree(elem.parentNode, dataLinkNames);
67
- } else {
68
- return dataLinkNames;
69
- }
70
- };
71
- return walkTree(el, dataLinkNames.slice());
31
+ /**
32
+ * Collects all `data-link-name` attribute values from the ancestor elements.
33
+ *
34
+ * @param {Element} el - The element to start searching from.
35
+ * @param {string[]} dataLinkNames - Initial array of data link names (optional).
36
+ * @returns {string[]} Array of `data-link-name` values.
37
+ */
38
+ const getDataLinkNames = (el, dataLinkNames = []) => {
39
+ if (!el || el === document.body || el === document) return dataLinkNames;
40
+
41
+ const dataLinkName = el.getAttribute('data-link-name');
42
+ if (dataLinkName) dataLinkNames.push(dataLinkName);
43
+
44
+ return getDataLinkNames(el.parentNode, dataLinkNames);
72
45
  };
73
46
 
74
47
  if (typeof document.addEventListener === 'function') {
75
48
  document.addEventListener(
76
49
  'click',
77
50
  function (e) {
78
- var anchorTarget, info;
51
+ let anchorTarget, info;
79
52
  anchorTarget = validAncestorAnchorElement(e.target);
80
53
  info = {
81
54
  from: [location.protocol, '//', location.host, location.pathname].join(
@@ -104,4 +77,4 @@ if (typeof document.addEventListener === 'function') {
104
77
 
105
78
  export default {
106
79
  getDataLinkNames: getDataLinkNames,
107
- };
80
+ };
@@ -3,6 +3,16 @@
3
3
  */
4
4
  const components = [];
5
5
 
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
6
16
  /**
7
17
  * theguardian.com uses a custom scroll event for performance reasons.
8
18
  * We pass in the event emitter so that we can listen to this custom scroll event.
@@ -196,14 +206,4 @@ export default {
196
206
  new Component(name, el, visibilityThreshold, reportingInterval),
197
207
  );
198
208
  },
199
- };
200
-
201
- /**
202
- * Exports for testing purposes.
203
- */
204
- export const _testExports = {
205
- startMonitoring,
206
- stopMonitoring,
207
- getAttentionTimes,
208
- components,
209
209
  };
@@ -1,20 +1,13 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var reportStatus;
3
-
4
1
  import core from './core.js';
5
-
6
2
  import './transmit.js';
7
-
8
3
  import './click-path-capture.js';
9
-
10
4
  import './perf.js';
11
-
12
5
  import './campaign.js';
13
6
 
14
- reportStatus = function (platform, status) {
7
+ const reportStatus = (platform, status) => {
15
8
  return core.init(platform, status);
16
9
  };
17
10
 
18
11
  export default {
19
- reportStatus: reportStatus,
20
- };
12
+ reportStatus,
13
+ };
@@ -1,56 +1,57 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var componentEventIsValid,
3
- handleIframeMessage,
4
- recordIframeClickEvent,
5
- recordIframeComponentEvent,
6
- retrieveOriginDomain,
7
- validIframeDomains;
8
-
9
1
  import clickCapture from './click-path-capture.js';
10
2
 
11
- validIframeDomains = [
3
+ const validIframeDomains = [
12
4
  '.theguardian.com',
13
5
  '.dev-theguardian.com',
14
6
  '.thegulocal.com', // NB - leading dot is essential here to avoid matching on non-Guardian-owned domains
15
7
  ];
16
8
 
17
- handleIframeMessage = function (messageEvent) {
18
- var eventObject, iFrame, originDomain;
19
- originDomain = retrieveOriginDomain(messageEvent.origin);
9
+ /**
10
+ * Handles messages received from iframes, recording events as necessary.
11
+ * @param {MessageEvent} messageEvent - The message event received from an iframe.
12
+ */
13
+ const handleIframeMessage = (messageEvent) => {
14
+ const originDomain = retrieveOriginDomain(messageEvent.origin);
20
15
  if (
21
16
  originDomain &&
22
- validIframeDomains.some((domain) => {
23
- return originDomain.endsWith(domain);
24
- })
17
+ validIframeDomains.some((domain) => originDomain.endsWith(domain))
25
18
  ) {
26
19
  if (messageEvent.data.type === 'ophan-iframe-click-event') {
27
- eventObject = messageEvent.data.value;
28
- iFrame = document.getElementById(messageEvent.data.iframeId);
20
+ const eventObject = messageEvent.data.value;
21
+ const iFrame = document.getElementById(messageEvent.data.iframeId);
29
22
  recordIframeClickEvent(iFrame, eventObject);
30
23
  } else if (messageEvent.data.type === 'ophan-iframe-component-event') {
31
- eventObject = messageEvent.data.value;
24
+ const eventObject = messageEvent.data.value;
32
25
  recordIframeComponentEvent(eventObject);
33
26
  }
34
27
  }
35
28
  };
36
29
 
37
- retrieveOriginDomain = function (eventOrigin) {
38
- var e, originURL;
30
+ /**
31
+ * Retrieves the domain from an event's origin URL.
32
+ * @param {string} eventOrigin - The origin URL of the event.
33
+ * @returns {(string|null)} The domain of the origin URL or null if it cannot be parsed.
34
+ */
35
+ const retrieveOriginDomain = (eventOrigin) => {
39
36
  try {
40
- originURL = new URL(eventOrigin);
37
+ const originURL = new URL(eventOrigin);
41
38
  return originURL.host;
42
39
  } catch (error) {
43
- e = error;
44
- if (e instanceof TypeError) {
40
+ if (error instanceof TypeError) {
45
41
  // We failed to parse origin as an URL, we should ignore the event
46
42
  return null;
47
43
  } else {
48
- throw e;
44
+ throw error;
49
45
  }
50
46
  }
51
47
  };
52
48
 
53
- recordIframeClickEvent = function (iFrame, clickEvent) {
49
+ /**
50
+ * Records a click event occurring within an iframe.
51
+ * @param {HTMLElement} iFrame - The iframe element where the click event occurred.
52
+ * @param {Object} clickEvent - The click event object to record.
53
+ */
54
+ const recordIframeClickEvent = (iFrame, clickEvent) => {
54
55
  if (iFrame) {
55
56
  clickEvent.clickLinkNames =
56
57
  clickCapture.getDataLinkNames(iFrame, clickEvent.clickLinkNames) ||
@@ -59,24 +60,29 @@ recordIframeClickEvent = function (iFrame, clickEvent) {
59
60
  window.guardian.ophan.record(clickEvent);
60
61
  };
61
62
 
62
- componentEventIsValid = function (eventObject) {
63
- if (!eventObject || typeof eventObject !== 'object') {
64
- return false;
65
- }
66
- if (
67
- !eventObject.componentEvent ||
68
- typeof eventObject.componentEvent !== 'object'
69
- ) {
70
- return false;
71
- }
72
- return true;
63
+ /**
64
+ * Validates if an event object is a valid component event.
65
+ * @param {Object} eventObject - The event object to validate.
66
+ * @returns {boolean} True if the event object is valid, false otherwise.
67
+ */
68
+ const componentEventIsValid = (eventObject) => {
69
+ return (
70
+ eventObject &&
71
+ typeof eventObject === 'object' &&
72
+ eventObject.componentEvent &&
73
+ typeof eventObject.componentEvent === 'object'
74
+ );
73
75
  };
74
76
 
75
- recordIframeComponentEvent = function (eventObject) {
77
+ /**
78
+ * Records a component event from an iframe.
79
+ * @param {Object} eventObject - The component event object to record.
80
+ */
81
+ const recordIframeComponentEvent = (eventObject) => {
76
82
  if (!componentEventIsValid(eventObject)) {
77
83
  return;
78
84
  }
79
85
  window.guardian.ophan.record(eventObject);
80
86
  };
81
87
 
82
- window.addEventListener('message', handleIframeMessage, false);
88
+ window.addEventListener('message', handleIframeMessage, false);
@@ -1,25 +1,17 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var ref;
3
-
4
1
  import core from './core.js';
5
-
6
2
  import transmit from './transmit.js';
7
-
8
3
  import attention from './attention.js';
9
-
10
4
  import './click-path-capture.js';
11
5
 
12
- if (((ref = window.guardian) != null ? ref.ophan : void 0) != null) {
13
- window.guardian.ophan;
6
+ if (window.guardian?.ophan) {
7
+ guardian.ophan;
14
8
  } else {
15
9
  core.init('embed');
16
- if (window.guardian == null) {
17
- window.guardian = {};
18
- }
19
- window.guardian.ophan = {
10
+ window.guardian = guardian || {};
11
+ guardian.ophan = {
20
12
  setEventEmitter: attention.setEventEmitter,
21
13
  trackComponentAttention: attention.initComponent,
22
14
  record: transmit.sendMore,
23
15
  viewId: transmit.viewId,
24
16
  };
25
- }
17
+ }
@@ -1,50 +1,38 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
- var exports, sendInitialEvent;
3
-
4
2
  import core from './core.js';
5
-
6
3
  import transmit from './transmit.js';
7
-
8
4
  import attention from './attention.js';
9
-
10
5
  import visibility from './visibility.js';
11
6
 
12
7
  import './click-path-capture.js';
13
-
14
8
  import './perf.js';
15
-
16
9
  import './campaign.js';
17
10
 
18
11
  core.init('manage-my-account');
19
12
 
20
- if (window.addEventListener != null) {
13
+ if (window.addEventListener) {
21
14
  attention.init(visibility);
22
15
  }
23
16
 
24
- sendInitialEvent = function (
17
+ const sendInitialEvent = (
25
18
  url = location.href,
26
- referrer = window.document.referrer,
27
- ) {
28
- var e;
19
+ referrer = document.referrer,
20
+ ) => {
29
21
  try {
30
22
  transmit.bumpViewId();
31
- return core.sendInitialEvent(null, url, referrer);
23
+ core.sendInitialEvent(null, url, referrer);
32
24
  } catch (error) {
33
- e = error;
34
- return console.log(e);
25
+ console.log(error);
35
26
  }
36
27
  };
37
28
 
38
- exports = {
39
- sendInitialEvent: sendInitialEvent,
29
+ const exports = {
30
+ sendInitialEvent,
40
31
  record: transmit.sendMore,
41
32
  viewId: transmit.viewId,
42
33
  };
43
34
 
44
- if (window.guardian == null) {
45
- window.guardian = {};
46
- }
47
-
35
+ window.guardian = window.guardian || {};
48
36
  window.guardian.ophan = exports;
49
37
 
50
- export default exports;
38
+ export default exports;
package/assets/perf.js CHANGED
@@ -1,18 +1,15 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
- var perf, sendTimingData;
3
-
4
2
  import core from './core.js';
5
3
 
6
- perf =
4
+ const perf =
7
5
  window.performance ||
8
6
  window.msPerformance ||
9
7
  window.webkitPerformance ||
10
8
  window.mozPerformance;
11
9
 
12
- sendTimingData = function () {
13
- var t;
14
- t = perf != null ? perf.timing : void 0;
15
- if (t != null) {
10
+ const sendTimingData = () => {
11
+ const t = perf?.timing;
12
+ if (t) {
16
13
  return {
17
14
  performance: {
18
15
  // Time required for domain lookup.
@@ -40,4 +37,4 @@ sendTimingData = function () {
40
37
 
41
38
  core.onLoadCapture(sendTimingData);
42
39
 
43
- export default perf;
40
+ export default perf;
@@ -1,48 +1,41 @@
1
- // Generated by CoffeeScript 2.7.0
2
- // Check if a visitor is using private browsing mode (true or false)
3
- // User agent checking and browser mode checking based on: https://gist.github.com/cou929/7973956#gistcomment-1769682
4
- var privateBrowsingMode;
5
-
6
1
  import transmit from './transmit.js';
7
2
 
8
- privateBrowsingMode = function (callback) {
9
- var reportNormalMode, reportPrivateMode, tryIndexedDB, tryLocalStorage;
10
- reportNormalMode = function () {
11
- return callback(false);
12
- };
13
- reportPrivateMode = function () {
14
- return callback(true);
15
- };
16
- tryLocalStorage = function () {
17
- var error;
3
+ /*
4
+ TO-DO:
5
+ This function could probably be simplified with an async function.
6
+ It basically returns a `boolean`, but uses a callback to do so... I can only guess it’s because of [`requestFileSystem`’s signature](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestFileSystem).
7
+ */
8
+ const privateBrowsingMode = (callback) => {
9
+ const reportNormalMode = () => callback(false);
10
+ const reportPrivateMode = () => callback(true);
11
+
12
+ const tryLocalStorage = () => {
18
13
  try {
19
14
  if (localStorage.length) {
20
- return reportNormalMode();
15
+ reportNormalMode();
21
16
  } else {
22
17
  localStorage.x = 1;
23
18
  localStorage.removeItem('x');
24
- return reportNormalMode();
19
+ reportNormalMode();
25
20
  }
26
- } catch (error1) {
27
- error = error1;
28
- return reportPrivateMode();
21
+ } catch (error) {
22
+ reportPrivateMode();
29
23
  }
30
24
  };
31
- tryIndexedDB = function () {
32
- var db, error;
25
+
26
+ const tryIndexedDB = () => {
33
27
  try {
34
- db = indexedDB.open('test');
28
+ const db = indexedDB.open('test');
35
29
  db.onsuccess = reportNormalMode;
36
- return (db.onerror = reportPrivateMode);
37
- } catch (error1) {
38
- error = error1;
39
- return reportPrivateMode();
30
+ db.onerror = reportPrivateMode;
31
+ } catch (error) {
32
+ reportPrivateMode();
40
33
  }
41
34
  };
42
35
 
43
36
  // Blink
44
37
  if (window.webkitRequestFileSystem) {
45
- return window.webkitRequestFileSystem(
38
+ window.webkitRequestFileSystem(
46
39
  window.TEMPORARY,
47
40
  1,
48
41
  reportNormalMode,
@@ -50,30 +43,29 @@ privateBrowsingMode = function (callback) {
50
43
  );
51
44
  // Firefox
52
45
  } else if ('MozAppearance' in document.documentElement.style) {
53
- return tryIndexedDB();
46
+ tryIndexedDB();
54
47
  // Safari
55
48
  } else if (/constructor/i.test(window.HTMLElement)) {
56
- return tryLocalStorage();
49
+ tryLocalStorage();
57
50
  } else if (
58
51
  !window.indexedDB &&
59
52
  (window.PointerEvent || window.MSPointerEvent)
60
53
  ) {
61
- return reportPrivateMode();
54
+ reportPrivateMode();
62
55
  } else {
63
56
  // Rest
64
- return reportNormalMode();
57
+ reportNormalMode();
65
58
  }
66
59
  };
67
60
 
68
61
  export default {
69
- init: function () {
70
- return privateBrowsingMode(function (value) {
71
- var report;
72
- report = {
62
+ init() {
63
+ privateBrowsingMode((value) => {
64
+ const report = {
73
65
  inPrivateBrowsingMode: value,
74
66
  };
75
- return transmit.sendMore(report);
67
+ transmit.sendMore(report);
76
68
  });
77
69
  },
78
- privateBrowsingMode: privateBrowsingMode,
79
- };
70
+ privateBrowsingMode,
71
+ };
package/assets/support.js CHANGED
@@ -1,44 +1,34 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var init, sendInitialEvent;
3
-
4
1
  import core from './core.js';
5
-
6
2
  import transmit from './transmit.js';
7
-
8
3
  import attention from './attention.js';
9
-
10
4
  import visibility from './visibility.js';
11
5
 
12
6
  import './click-path-capture.js';
13
-
14
7
  import './perf.js';
15
-
16
8
  import './campaign.js';
17
9
 
18
- init = function () {
10
+ const init = () => {
19
11
  core.init('support');
20
- if (window.addEventListener != null) {
21
- return attention.init(visibility);
12
+ if (window.addEventListener) {
13
+ attention.init(visibility);
22
14
  }
23
15
  };
24
16
 
25
- sendInitialEvent = function (
17
+ const sendInitialEvent = (
26
18
  url = location.href,
27
- referrer = window.document.referrer,
28
- ) {
29
- var e;
19
+ referrer = document.referrer,
20
+ ) => {
30
21
  try {
31
22
  transmit.bumpViewId();
32
- return core.sendInitialEvent(null, url, referrer);
23
+ core.sendInitialEvent(null, url, referrer);
33
24
  } catch (error) {
34
- e = error;
35
- return console.log(e);
25
+ console.log(error);
36
26
  }
37
27
  };
38
28
 
39
29
  export default {
40
- init: init,
41
- sendInitialEvent: sendInitialEvent,
30
+ init,
31
+ sendInitialEvent,
42
32
  record: transmit.sendMore,
43
33
  viewId: transmit.viewId,
44
- };
34
+ };
@@ -1,64 +1,47 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var buildQueryString,
3
- bumpViewId,
4
- generatePageViewId,
5
- isDefined,
6
- ophanRemoteHost,
7
- ref,
8
- ref1,
9
- ref2,
10
- send,
11
- sendInitial,
12
- sendMore,
13
- smartEncode,
14
- viewId,
15
- hasProp = {}.hasOwnProperty;
16
-
17
- ophanRemoteHost =
18
- (typeof window !== 'undefined' && window !== null
19
- ? window.ophanRemoteHost
20
- : void 0) || 'https://ophan.theguardian.com';
21
-
22
- generatePageViewId = function () {
23
- return (
24
- new Date().getTime().toString(36) +
25
- 'xxxxxxxxxxxx'.replace(/x/g, function () {
26
- return Math.floor(Math.random() * 36).toString(36);
27
- })
28
- );
1
+ const ophanRemoteHost = window?.ophanRemoteHost || '//ophan.theguardian.com';
2
+
3
+ /**
4
+ * @returns {string}
5
+ */
6
+ const generatePageViewId = () => {
7
+ return `${new Date().getTime().toString(36)}${'xxxxxxxxxxxx'.replace(
8
+ /x/g,
9
+ () => Math.floor(Math.random() * 36).toString(36),
10
+ )}`;
29
11
  };
30
12
 
31
13
  // guardian.config.ophan.pageViewId is now pregenerated in
32
14
  // the Guardian Frontend Project (https://github.com/guardian/frontend)
33
15
  // frontend/common/app/templates/inlineJS/blocking/config.scala.js
34
16
  // If you're going to change the generation method, please update that one too
35
- viewId =
36
- (ref =
37
- typeof guardian !== 'undefined' && guardian !== null
38
- ? (ref1 = guardian.config) != null
39
- ? (ref2 = ref1.ophan) != null
40
- ? ref2.pageViewId
41
- : void 0
42
- : void 0
43
- : void 0) != null
44
- ? ref
45
- : generatePageViewId();
46
-
47
- bumpViewId = function () {
48
- return (viewId = generatePageViewId());
17
+ let viewId;
18
+ if (typeof guardian !== 'undefined') {
19
+ viewId = guardian.config?.ophan?.pageViewId ?? generatePageViewId();
20
+ } else {
21
+ viewId = generatePageViewId();
22
+ }
23
+
24
+ const bumpViewId = () => {
25
+ viewId = generatePageViewId();
49
26
  };
50
27
 
51
- // send the initial event associated with this page view
52
- // store the resulting page view id
53
- // this must only ever be called *once* subsequent calls will
54
- // be ignored
55
- sendInitial = function (obj) {
56
- return send(ophanRemoteHost + `/img/1?${buildQueryString(obj)}`);
28
+ /**
29
+ * send the initial event associated with this page view
30
+ * store the resulting page view id
31
+ * this must only ever be called *once* subsequent calls will
32
+ * be ignored
33
+ * @param {Object} obj - The data to be sent.
34
+ */
35
+ const sendInitial = (obj) => {
36
+ send(`${ophanRemoteHost}/img/1?${buildQueryString(obj)}`);
57
37
  };
58
38
 
59
- // send more data - if initial hasn't been sent, queue up and wait for the
60
- // initial send to occur
61
- smartEncode = function (value) {
39
+ /**
40
+ * Encodes a value for URL transmission.
41
+ * @param {(string|number)} value - The value to encode.
42
+ * @returns {string} The encoded value.
43
+ */
44
+ const smartEncode = (value) => {
62
45
  if (typeof value === 'string' || typeof value === 'number') {
63
46
  return encodeURIComponent(value);
64
47
  } else {
@@ -66,85 +49,95 @@ smartEncode = function (value) {
66
49
  }
67
50
  };
68
51
 
69
- sendMore = function (obj, f) {
70
- var prop, url, value, values;
71
- if (!JSON) {
72
- return;
73
- }
74
- values = (function () {
75
- var results;
76
- results = [];
77
- for (prop in obj) {
78
- if (!hasProp.call(obj, prop)) continue;
79
- value = obj[prop];
80
- if (isDefined(value)) {
81
- results.push(`${encodeURIComponent(prop)}=${smartEncode(value)}`);
82
- }
52
+ /**
53
+ * Used for sending additional events after the initial PageView event has been sent, such as attention events.
54
+ * @param {Object} obj - The data object to be sent.
55
+ * @param {Function} [f] - An optional callback function. This is used by consumers of this library.
56
+ */
57
+ const sendMore = (obj, f) => {
58
+ if (!JSON) return;
59
+
60
+ const values = [];
61
+ for (const prop in obj) {
62
+ if (obj.hasOwnProperty(prop) && isDefined(obj[prop])) {
63
+ values.push(`${encodeURIComponent(prop)}=${smartEncode(obj[prop])}`);
83
64
  }
84
- return results;
85
- })();
86
- if (!values.length) {
65
+ }
66
+
67
+ if (values.length === 0) {
87
68
  return;
88
69
  }
89
- url = `${ophanRemoteHost}/img/2?viewId=${viewId}&${values.join('&')}`;
90
- return send(url, f);
70
+
71
+ const url = `${ophanRemoteHost}/img/2?viewId=${viewId}&${values.join('&')}`;
72
+ send(url, f);
91
73
  };
92
74
 
93
- isDefined = function (v) {
75
+ /**
76
+ * Checks if a value is defined and not null.
77
+ * @param {*} v - The value to check.
78
+ * @returns {boolean} True if the value is defined, false otherwise.
79
+ */
80
+ const isDefined = (v) => {
94
81
  if (Array.isArray(v)) {
95
- return v.length;
82
+ return !!v.length;
96
83
  } else {
97
84
  return v != null;
98
85
  }
99
86
  };
100
87
 
101
- buildQueryString = function (obj) {
102
- var prop, value, values;
88
+ /**
89
+ * Builds a query string from an object.
90
+ * @param {Object} obj - The object to convert to a query string.
91
+ * @returns {string} The resulting query string.
92
+ */
93
+ const buildQueryString = (obj) => {
103
94
  obj.viewId = viewId;
104
- values = (function () {
105
- var results;
106
- results = [];
107
- for (prop in obj) {
108
- if (!hasProp.call(obj, prop)) continue;
109
- value = obj[prop];
110
- if (value != null) {
111
- results.push(
112
- `${encodeURIComponent(prop)}=${encodeURIComponent(value)}`,
113
- );
114
- }
95
+
96
+ const values = [];
97
+ for (const prop in obj) {
98
+ if (obj.hasOwnProperty(prop) && obj[prop] != null) {
99
+ values.push(`${encodeURIComponent(prop)}=${encodeURIComponent(obj[prop])}`);
115
100
  }
116
- return results;
117
- })();
101
+ }
102
+
118
103
  return values.join('&');
119
104
  };
120
105
 
121
- send = function (url, f) {
122
- var checker, fetchOptions, image;
106
+ /**
107
+ * Sends data to a specified URL, i.e.: the Ophan Tracker backend service.
108
+ * @param {string} url - The URL to send data to.
109
+ * @param {() => void} [f] - An optional callback function.
110
+ */
111
+ const send = (url, f) => {
123
112
  if (typeof fetch === 'function') {
124
- fetchOptions = {
125
- method: 'GET',
126
- keepalive: true,
127
- };
128
- return fetch(url, fetchOptions).then(function () {
129
- return typeof f === 'function' ? f() : void 0;
130
- });
113
+ return fetch(url, { method: 'GET', mode: 'no-cors', keepalive: true, credentials: "include" }).then(() => f?.());
131
114
  } else {
132
- image = new Image();
133
- checker = setInterval(function () {
115
+ const image = new Image();
116
+ const checker = setInterval(() => {
134
117
  if (image.complete) {
135
118
  if (typeof f === 'function') {
136
119
  f();
137
120
  }
138
- return clearInterval(checker);
121
+ clearInterval(checker);
139
122
  }
140
123
  }, 10);
141
- return (image.src = url);
124
+ image.src = url;
142
125
  }
143
126
  };
144
127
 
145
128
  export default {
146
- sendInitial: sendInitial,
147
- sendMore: sendMore,
148
- viewId: viewId,
149
- bumpViewId: bumpViewId,
129
+ sendInitial,
130
+ sendMore,
131
+ viewId,
132
+ bumpViewId,
150
133
  };
134
+
135
+ /**
136
+ * Exports for testing purposes.
137
+ */
138
+ export const _testExports = {
139
+ generatePageViewId,
140
+ smartEncode,
141
+ isDefined,
142
+ buildQueryString,
143
+ };
@@ -1,20 +1,23 @@
1
- // Generated by CoffeeScript 2.7.0
2
1
  export default {
3
- state: function () {
4
- return (
5
- document.visibilityState ||
6
- document.webkitVisibilityState ||
7
- document.mozVisibilityState ||
8
- document.msVisibilityState
9
- );
10
- },
11
- changeEvent: document.visibilityState
12
- ? 'visibilitychange'
13
- : document.webkitVisibilityState
14
- ? 'webkitvisibilitychange'
15
- : document.mozVisibilityState
16
- ? 'mozvisibilitychange'
17
- : document.msVisibilityState
18
- ? 'msvisibilitychange'
19
- : void 0,
2
+ state: () =>
3
+ document.visibilityState ||
4
+ document.webkitVisibilityState ||
5
+ document.mozVisibilityState ||
6
+ document.msVisibilityState,
7
+
8
+ changeEvent: getChangeEvent(),
9
+
20
10
  };
11
+
12
+ function getChangeEvent() {
13
+ if (document.visibilityState) {
14
+ return 'visibilitychange';
15
+ } else if (document.webkitVisibilityState) {
16
+ return 'webkitvisibilitychange';
17
+ } else if (document.mozVisibilityState) {
18
+ return 'mozvisibilitychange';
19
+ } else if (document.msVisibilityState) {
20
+ return 'msvisibilitychange';
21
+ }
22
+ return undefined;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guardian/ophan-tracker-js",
3
- "version": "2.1.0-next.1",
3
+ "version": "2.1.0-next.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=16"
package/readme.md CHANGED
@@ -10,7 +10,7 @@ $ npm install ophan-tracker-js
10
10
 
11
11
  ## Contribute
12
12
 
13
- #### Proxy tracker-ts:
13
+ #### Proxy tracker-js:
14
14
 
15
15
  1. Install dependencies
16
16
 
@@ -19,28 +19,22 @@ $ npm install ophan-tracker-js
19
19
  .../ophan/tracker-js $ npm install
20
20
  ```
21
21
 
22
- 2. In one terminal window:
23
-
24
- ```
25
- .../ophan/tracker-js $ npm run coffee-to-esm:watch
26
- ```
27
-
28
- 3. In another terminal window:
22
+ 2. In another terminal window:
29
23
 
30
24
  ```
31
25
  .../ophan/tracker-js $ lighttpd -f lighttpd.conf -D
32
26
  ```
33
27
 
34
- 4. Install FoxyProxy (https://getfoxyproxy.org/downloads/) in your browser and create a rule that forces j.ophan.co.uk
28
+ 3. Install FoxyProxy (https://getfoxyproxy.org/downloads/) in your browser and create a rule that forces j.ophan.co.uk
35
29
  to localhost 8000. (For me FoxyProxy needed a few switches on and off before it decided to start talking to lighttpd.)
36
30
 
37
- 5. In another terminal window, start a server that is [running the Guardian frontend](https://github.com/guardian/frontend/blob/main/docs/01-start-here/01-installation-steps.md)
31
+ 4. In another terminal window, start a server that is [running the Guardian frontend](https://github.com/guardian/frontend/blob/main/docs/01-start-here/01-installation-steps.md)
38
32
 
39
- 6. Navigate to where you are running the Guardian locally (e.g. https://localhost:9000/uk)
33
+ 5. Navigate to where you are running the Guardian locally (e.g. https://localhost:9000/uk)
40
34
 
41
- 7. Ensure FoxyProxy is enabled
35
+ 6. Ensure FoxyProxy is enabled
42
36
 
43
- 8. Develop with pleasure
37
+ 7. Develop with pleasure
44
38
 
45
39
  ## Publish
46
40
 
@@ -62,8 +56,14 @@ We use `changesets` for automated publishing of the NPM package:
62
56
 
63
57
  3. When the PR is merged, the [`changesets` github action](https://github.com/changesets/action) will create a new PR, [example here](https://github.com/guardian/ophan/pull/5626). On merging this generated PR, the action will publish to NPM. (This checks the subdirectory correctly.)
64
58
 
65
- _Note:_ This seems like an unnecessary extra step and another PR to merge. This action is useful for multi-package repos, and packages with more frequent changes where it consolidates changes from multiple PR's into a single release. This therefore does not apply to us, but we've had issues publishing straight to NPM via the initial action on merge to main.
66
-
67
59
  ##### S3 Bucket
68
60
 
69
61
  * [https://j.ophan.co.uk/](https://j.ophan.co.uk/jobs.js) - CDN/S3, [deployed through RiffRaff](https://riffraff.gutools.co.uk/deployment/history?projectName=ophan%3A%3Aophan-tracker-js&page=1) and used by some Guardian sites like https://jobs.theguardian.com/. An advantage of this approach is that sites using it immediately get Tracker JS updates, without developer intervention.
62
+
63
+ ##### Rollup
64
+
65
+ The generated files from Rollup are only used for publishing to the CDN to use SystemJS / AMD
66
+
67
+ ##### Example Usage
68
+
69
+ You can see example usage of the library on theguardian.com [here](https://github.com/guardian/dotcom-rendering/blob/main/dotcom-rendering/src/client/ophan/ophan.ts)