@guardian/ophan-tracker-js 2.0.3 → 2.1.0-next.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.
package/package.json CHANGED
@@ -1,26 +1,30 @@
1
1
  {
2
- "name": "@guardian/ophan-tracker-js",
3
- "version": "2.0.3",
4
- "type": "module",
5
- "engines": {
6
- "node": ">=16"
7
- },
8
- "files": [
9
- "build/*.js",
10
- "build/vendor/*.js"
11
- ],
12
- "main": "build/ng.js",
13
- "devDependencies": {
14
- "@babel/preset-env": "7.22.10",
15
- "@changesets/cli": "^2.26.2",
16
- "@rollup/plugin-babel": "6.0.3",
17
- "@rollup/plugin-terser": "0.4.3",
18
- "coffeescript": "2.7.0",
19
- "rollup": "3.28.0"
20
- },
21
- "scripts": {
22
- "coffee-to-esm:watch": "coffee --compile --watch --output build/ assets/coffee/ophan/",
23
- "coffee-to-esm": "coffee --compile --output build/ assets/coffee/ophan/",
24
- "prepublishOnly": "npm run coffee-to-esm"
25
- }
2
+ "name": "@guardian/ophan-tracker-js",
3
+ "version": "2.1.0-next.0",
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=16"
7
+ },
8
+ "files": [
9
+ "build/*.js",
10
+ "build/vendor/*.js"
11
+ ],
12
+ "main": "build/ng.js",
13
+ "devDependencies": {
14
+ "@babel/preset-env": "7.22.10",
15
+ "@changesets/cli": "^2.26.2",
16
+ "@guardian/prettier": "^7.0.0",
17
+ "@rollup/plugin-babel": "6.0.3",
18
+ "@rollup/plugin-terser": "0.4.3",
19
+ "jest": "^29.7.0",
20
+ "jest-environment-jsdom": "^29.7.0",
21
+ "rollup": "3.28.0",
22
+ "rollup-jest": "^3.1.0"
23
+ },
24
+ "scripts": {
25
+ "build": "NODE_ENV=production npx rollup -c",
26
+ "build:test": "NODE_ENV=test npx rollup -c",
27
+ "test": "jest",
28
+ "format": "npx prettier --write ."
29
+ }
26
30
  }
package/readme.md CHANGED
@@ -22,9 +22,7 @@ $ npm install ophan-tracker-js
22
22
  2. In one terminal window:
23
23
 
24
24
  ```
25
- .../ophan/tracker-js $ grunt
26
- .../ophan/tracker-js $ grunt watch
27
-
25
+ .../ophan/tracker-js $ npm run coffee-to-esm:watch
28
26
  ```
29
27
 
30
28
  3. In another terminal window:
@@ -50,46 +48,22 @@ to localhost 8000. (For me FoxyProxy needed a few switches on and off before it
50
48
 
51
49
  When you make a change to Tracker JS, you must release on *both* these systems:
52
50
 
53
- * The NPM package [`@guardian/ophan-tracker-js`](https://www.npmjs.com/package/ophan-tracker-js), commonly used on internally developed Guardian sites like Guardian Frontend and Support Frontend.
54
- * [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.
55
-
56
- **Ensure that before you merge a change, you first bump the version in `package.json`. Please make sure you update the [changes.md](changes.md) file too. If you have added a new Ophan client to support a new platform, please add the path to the built output file to the files list in [`package.json`](https://github.com/guardian/ophan/blob/main/tracker-js/package.json#L4).**
57
-
58
- ### Publish to NPM
59
-
60
- Whenever the Tracker script is changed, [its `ophan-tracker-js` NPM package](https://www.npmjs.com/package/ophan-tracker-js/access) will
61
- need to be updated to a new version.
62
-
63
- If you don't already have an NPM account, be sure to set up two-factor authentification.
51
+ ##### NPM
64
52
 
65
- **Log into NPM**
53
+ We use `changesets` for automated publishing of the NPM package:
66
54
 
67
- ```
68
- $ npm login
69
- ```
70
-
71
- **Compile**
72
-
73
- ```
74
- .../ophan/tracker-js $ npm install
75
- ```
55
+ 1. Add a changeset, run the following command and follow the instructions
76
56
 
77
- **Publish**
57
+ ```
58
+ .../ophan/tracker-js $ npx changeset
59
+ ```
78
60
 
79
- ```
80
- .../ophan/tracker-js $ npm publish
81
- ```
61
+ 2. When you raise a PR, the changeset bot won’t correctly see that you have included a changeset – it only checks for the .changesets folder in the root of the repository, whereas we have it in a subdirectory. Don’t worry.
62
+
63
+ 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.)
82
64
 
83
- **You will need to be added as an owner of `ophan-tracker-js`. To find the current owners:**
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.
84
66
 
85
- ```
86
- $ npm owner ls @guardian/ophan-tracker-js
87
- ```
67
+ ##### S3 Bucket
88
68
 
89
- **To add someone as an owner:**
90
-
91
- ```
92
- npm owner add npm.username @guardian/ophan-tracker-js
93
- ```
94
-
95
- More information on setting owners is available in [NPM's documentation](https://docs.npmjs.com/cli/v7/commands/npm-owner).
69
+ * [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.
@@ -1,24 +0,0 @@
1
- // Generated by CoffeeScript 2.7.0
2
- import abd from "./vendor/adBlockDetectionLib.js";
3
-
4
- export default {
5
- run: function() {
6
- return new Promise(function(resolve, reject) {
7
- var e;
8
- if (abd.init) {
9
- try {
10
- return abd.init({
11
- complete: function(v) {
12
- return resolve(v);
13
- }
14
- });
15
- } catch (error) {
16
- e = error;
17
- return reject(e);
18
- }
19
- } else {
20
- return reject(new Error("Failed to initialise adblock detection"));
21
- }
22
- });
23
- }
24
- };
@@ -1,149 +0,0 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var ATTENTIONDECAY, EVENTS, REPORTINGINTERVAL, cancelDecayTimer, decayTimerId, incrementTotalAttentionTimeByUnrecordedAmount, initAttention, initComponent, makeActive, makeInactive, reportedTotalAttentionMs, reporter, setEventEmitter, totalAttentionMs, unrecordedAttentionStarted, videoPlaying;
3
-
4
- import transmit from "./transmit.js";
5
-
6
- import components from "./components.js";
7
-
8
- // The upworthy definition of attention minutes, from
9
- // http://upworthy.github.io/2014/06/implementing-attention-minutes-part-1/ :
10
-
11
- // "We consider the page to have the user’s attention if
12
- // 1. some activity is happening on the page. For our purposes this means:
13
- // 1. the page currently has focus
14
- // 2. and the user has interacted with the page within a certain timeout
15
- // 2. or a video is playing on the page
16
- // Attention Minutes, then, are the total number of minutes that a user has spent on
17
- // a page where the above conditions have held true."
18
-
19
- // our definition of "within a certain timeout" above:
20
- ATTENTIONDECAY = 5000;
21
-
22
- // our definition of "the user has interacted with the page"
23
- EVENTS = ['focus', 'click', 'scroll', 'mousemove', 'touchstart', 'touchend', 'touchcancel', 'touchleave', 'touchmove', 'keyup', 'keydown'];
24
-
25
- REPORTINGINTERVAL = 10000;
26
-
27
- // total attention time so far on this page
28
- totalAttentionMs = 0;
29
-
30
- // the time elapsed since the time origin (https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin)
31
- // when the most recent period of attention (that is, attention
32
- // which we haven't yet added to totalAttentionMs) began.
33
- // null if we don't have attention.
34
- unrecordedAttentionStarted = null;
35
-
36
- // the attention time we last reported to ophan
37
- reportedTotalAttentionMs = null; // setting null here forces a first report of attention time, even if it's only 0ms
38
-
39
-
40
- // the decay timer if we've set one
41
- decayTimerId = null;
42
-
43
- // is video currently playing?
44
- videoPlaying = false;
45
-
46
- cancelDecayTimer = function() {
47
- if (decayTimerId != null) {
48
- window.clearTimeout(decayTimerId);
49
- }
50
- return decayTimerId = null;
51
- };
52
-
53
- makeActive = function() {
54
- // mark when the latest period of attention began,
55
- // if we're not already in an attention period
56
- if (unrecordedAttentionStarted == null) {
57
- unrecordedAttentionStarted = performance.now();
58
- }
59
- components.startMonitoring();
60
- // start decay timer
61
- cancelDecayTimer();
62
- return decayTimerId = window.setTimeout((function() {
63
- if (!videoPlaying) {
64
- return makeInactive();
65
- }
66
- }), ATTENTIONDECAY);
67
- };
68
-
69
- incrementTotalAttentionTimeByUnrecordedAmount = function() {
70
- var now, unrecordedMs;
71
- if (unrecordedAttentionStarted != null) {
72
- now = performance.now();
73
- // the Math.min here is to deal with occasions where we don't get an event indicating that the user
74
- // has become inactive - this can happen e.g. due to users suspending their phone. Never send
75
- // crazy big values!
76
- unrecordedMs = Math.min(now - unrecordedAttentionStarted, REPORTINGINTERVAL);
77
- totalAttentionMs += unrecordedMs;
78
- return unrecordedAttentionStarted = now;
79
- }
80
- };
81
-
82
- makeInactive = function() {
83
- cancelDecayTimer();
84
- components.stopMonitoring();
85
- incrementTotalAttentionTimeByUnrecordedAmount();
86
- return unrecordedAttentionStarted = null;
87
- };
88
-
89
- reporter = function() {
90
- var componentAttentionTimes, report;
91
- incrementTotalAttentionTimeByUnrecordedAmount();
92
- if (totalAttentionMs !== reportedTotalAttentionMs) {
93
- report = {
94
- attentionMs: Math.round(totalAttentionMs)
95
- };
96
- componentAttentionTimes = components.getAttentionTimes();
97
- if (Object.keys(componentAttentionTimes).length) {
98
- report.componentAttentionMs = componentAttentionTimes;
99
- }
100
- transmit.sendMore(report);
101
- return reportedTotalAttentionMs = totalAttentionMs;
102
- }
103
- };
104
-
105
- initComponent = function(name, el, visibilityThreshold = 0.5) {
106
- return components.registerComponent(name, el, visibilityThreshold, REPORTINGINTERVAL);
107
- };
108
-
109
- initAttention = function(visibility) {
110
- var event, i, len;
111
- for (i = 0, len = EVENTS.length; i < len; i++) {
112
- event = EVENTS[i];
113
- // user interaction tracking
114
- window.addEventListener(event, makeActive);
115
- }
116
- // focus tracking
117
- document.addEventListener(visibility.changeEvent, (function() {
118
- if (visibility.state() === "visible") {
119
- return makeActive();
120
- } else {
121
- return makeInactive();
122
- }
123
- }), false);
124
- // video tracking
125
- document.addEventListener('videoPlaying', function() {
126
- videoPlaying = true;
127
- return makeActive();
128
- });
129
- document.addEventListener('videoEnded', function() {
130
- videoPlaying = false;
131
- return makeInactive();
132
- });
133
- document.addEventListener('videoPause', function() {
134
- videoPlaying = false;
135
- return makeInactive();
136
- });
137
- window.setTimeout(reporter, 100);
138
- return window.setInterval(reporter, REPORTINGINTERVAL);
139
- };
140
-
141
- setEventEmitter = function(emitter) {
142
- return components.setEventEmitter(emitter);
143
- };
144
-
145
- export default {
146
- init: initAttention,
147
- initComponent: initComponent,
148
- setEventEmitter: setEventEmitter
149
- };
package/build/campaign.js DELETED
@@ -1,30 +0,0 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var ref, ref1, start;
3
-
4
- import transmit from "./transmit.js";
5
-
6
- import perf from "./perf.js";
7
-
8
- start = (ref = perf != null ? (ref1 = perf.timing) != null ? ref1.domComplete : void 0 : void 0) != null ? ref : performance.now();
9
-
10
- // DFP ads
11
- if (typeof googletag !== "undefined" && googletag !== null) {
12
- googletag.cmd.push(function() {
13
- return googletag.pubads().addEventListener('slotRenderEnded', function(event) {
14
- var slotId;
15
- slotId = event.slot.getSlotId().getDomId();
16
- return transmit.sendMore({
17
- "ads": [
18
- {
19
- slot: slotId,
20
- campaignId: event.isEmpty ? "__empty__" : event.lineItemId,
21
- creativeId: event.creativeId,
22
- // overall time to render an ad
23
- timeToRenderEnded: Math.round(performance.now() - start),
24
- adServer: "DFP"
25
- }
26
- ]
27
- });
28
- });
29
- });
30
- }
@@ -1,81 +0,0 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var getContainingComponent, getDataLinkNames, validAncestorAnchorElement;
3
-
4
- import ophan from "./core.js";
5
-
6
- import transmit from "./transmit.js";
7
-
8
- validAncestorAnchorElement = function(el) {
9
- var elementType, ref;
10
- elementType = el != null ? (ref = el.nodeName) != null ? ref.toLowerCase() : void 0 : void 0;
11
- if (elementType === "a") {
12
- return el;
13
- } else if ((elementType == null) || elementType === "body") {
14
- return null;
15
- } else {
16
- return validAncestorAnchorElement(el.parentNode);
17
- }
18
- };
19
-
20
- getContainingComponent = function(el) {
21
- var elementType, ref;
22
- elementType = el != null ? (ref = el.nodeName) != null ? ref.toLowerCase() : void 0 : void 0;
23
- if ((elementType == null) || elementType === "body") {
24
- return null;
25
- } else {
26
- return (typeof el.getAttribute === "function" ? el.getAttribute("data-component") : void 0) || getContainingComponent(el.parentNode);
27
- }
28
- };
29
-
30
- getDataLinkNames = function(el, dataLinkNames = []) {
31
- var isBody, isDocument, walkTree;
32
- isBody = function(elem) {
33
- var ref;
34
- return (elem != null ? (ref = elem.nodeName) != null ? ref.toLowerCase() : void 0 : void 0) === document.body.nodeName.toLowerCase();
35
- };
36
- isDocument = function(elem) {
37
- return elem === document;
38
- };
39
- walkTree = function(elem, dataLinkNames) {
40
- var dataLinkName;
41
- if ((elem != null) && !isBody(elem) && !isDocument(elem)) {
42
- dataLinkName = elem.getAttribute("data-link-name");
43
- if (dataLinkName != null) {
44
- dataLinkNames.push(dataLinkName);
45
- }
46
- return walkTree(elem.parentNode, dataLinkNames);
47
- } else {
48
- return dataLinkNames;
49
- }
50
- };
51
- return walkTree(el, dataLinkNames.slice());
52
- };
53
-
54
- if (typeof document.addEventListener === "function") {
55
- document.addEventListener("click", function(e) {
56
- var anchorTarget, info;
57
- anchorTarget = validAncestorAnchorElement(e.target);
58
- info = {
59
- from: [location.protocol, "//", location.host, location.pathname].join(""),
60
- to: anchorTarget ? anchorTarget.href : void 0,
61
- referringComponent: getContainingComponent(e.target),
62
- referringDataLinkNames: getDataLinkNames(e.target),
63
- refPlatform: ophan.servingPlatform(),
64
- refViewId: ophan.viewId
65
- };
66
- if (info.referringDataLinkNames != null) {
67
- transmit.sendMore({
68
- clickComponent: info.referringComponent,
69
- clickLinkNames: info.referringDataLinkNames
70
- });
71
- }
72
- if (anchorTarget != null) {
73
- // then we may be about to navigate away from this page, so store to send on next request
74
- return ophan.storeDataToSendOnNextEvent(info);
75
- }
76
- }, false);
77
- }
78
-
79
- export default {
80
- getDataLinkNames: getDataLinkNames
81
- };
@@ -1,167 +0,0 @@
1
- // Generated by CoffeeScript 2.7.0
2
- var Component, components, eventEmitter;
3
-
4
- components = [];
5
-
6
- // theguardian.com uses a custom scroll event for performance reasons.
7
- // We pass in the event emitter so that we can listen to this custom scroll event.
8
- // If not on theguardian.com, we default to the native scroll event
9
- eventEmitter = null;
10
-
11
- // Tracks attention time of elements on the page with data-component attributes.
12
- // This should always be less than or equal to the page attention time.
13
- // If the page does not have attention, no component has attention.
14
- // If the page does have attention, a given component *may* also have
15
- // attention, if it is within the viewport and not hidden.
16
- Component = class Component {
17
- constructor(name1, element, visibilityThreshold1, reportingInterval1) {
18
- this.name = name1;
19
- this.element = element;
20
- this.visibilityThreshold = visibilityThreshold1;
21
- this.reportingInterval = reportingInterval1;
22
- this.visible = false;
23
- // total attention time so far for this element
24
- this.totalAttentionMs = 0;
25
- // the time elapsed since the time origin (https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin)
26
- // when the most recent period of attention (that is, attention
27
- // which we haven't yet added to totalAttentionMs) began.
28
- // null if we don't have attention.
29
- this.unrecordedAttentionStarted = null;
30
- // the attention time when getAttentionTime() was last called
31
- this.reportedTotalAttentionMs = 0;
32
- this.usingEmitter = false;
33
- this.visCheck = this.checkVisibility.bind(this);
34
- if (eventEmitter != null) {
35
- this.usingEmitter = true;
36
- eventEmitter.on('window:throttledScroll', this.visCheck);
37
- } else {
38
- window.addEventListener('scroll', this.visCheck);
39
- }
40
- window.addEventListener('resize', this.visCheck);
41
- }
42
-
43
- isVisible(threshold = 1) {
44
- var box, height, width, windowHeight, windowWidth;
45
- if (!this.element.offsetParent) {
46
- // exclude hidden elements
47
- // will also exclude position: fixed elements but we don't care about these
48
- // (because their attention time would always be the same as the whole page)
49
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
50
- return false;
51
- } else {
52
- box = this.element.getBoundingClientRect();
53
- width = box.right - box.left;
54
- height = box.bottom - box.top;
55
- windowHeight = window.innerHeight || document.documentElement.clientHeight;
56
- windowWidth = window.innerWidth || document.documentElement.clientWidth;
57
- return box.left >= -(width * (1 - threshold)) && box.top >= -(height * (1 - threshold)) && box.right <= windowWidth + (width * (1 - threshold)) && box.bottom <= windowHeight + (height * (1 - threshold));
58
- }
59
- }
60
-
61
- visibilityHasChanged() {
62
- var wasVisible;
63
- wasVisible = this.visible;
64
- this.visible = this.isVisible(this.visibilityThreshold);
65
- return wasVisible !== this.visible;
66
- }
67
-
68
- rebindToEventEmitter() {
69
- if (!this.usingEmitter && (eventEmitter != null)) {
70
- window.removeEventListener('scroll', this.visCheck);
71
- this.usingEmitter = true;
72
- return eventEmitter.on('window:throttledScroll', this.visCheck);
73
- }
74
- }
75
-
76
- checkVisibility() {
77
- this.rebindToEventEmitter();
78
- if (this.visibilityHasChanged()) {
79
- if (this.visible) {
80
- return this.makeActive();
81
- } else {
82
- return this.makeInactive();
83
- }
84
- }
85
- }
86
-
87
- makeActive() {
88
- // mark when the latest period of attention began,
89
- // if we're not already in an attention period
90
- return this.unrecordedAttentionStarted != null ? this.unrecordedAttentionStarted : this.unrecordedAttentionStarted = performance.now();
91
- }
92
-
93
- makeInactive() {
94
- this.incrementTotalAttentionTimeByUnrecordedAmount();
95
- return this.unrecordedAttentionStarted = null;
96
- }
97
-
98
- hadAttentionSinceLastGet() {
99
- this.incrementTotalAttentionTimeByUnrecordedAmount();
100
- return this.totalAttentionMs !== this.reportedTotalAttentionMs;
101
- }
102
-
103
- getAttentionTime() {
104
- this.incrementTotalAttentionTimeByUnrecordedAmount();
105
- this.reportedTotalAttentionMs = this.totalAttentionMs;
106
- return this.totalAttentionMs;
107
- }
108
-
109
- incrementTotalAttentionTimeByUnrecordedAmount() {
110
- var now, unrecordedMs;
111
- if (this.unrecordedAttentionStarted != null) {
112
- now = performance.now();
113
- // the Math.min here is to deal with occasions where we don't get an event indicating that the user
114
- // has become inactive - this can happen e.g. due to users suspending their phone. Never send
115
- // crazy big values!
116
- unrecordedMs = Math.min(now - this.unrecordedAttentionStarted, this.reportingInterval);
117
- this.totalAttentionMs += unrecordedMs;
118
- return this.unrecordedAttentionStarted = now;
119
- }
120
- }
121
-
122
- };
123
-
124
- export default {
125
- setEventEmitter: function(emitter) {
126
- return eventEmitter = emitter;
127
- },
128
- startMonitoring: function() {
129
- var component, i, len, results;
130
- results = [];
131
- for (i = 0, len = components.length; i < len; i++) {
132
- component = components[i];
133
- if (component.isVisible()) {
134
- results.push(component.makeActive());
135
- }
136
- }
137
- return results;
138
- },
139
- stopMonitoring: function() {
140
- var component, i, len, results;
141
- results = [];
142
- for (i = 0, len = components.length; i < len; i++) {
143
- component = components[i];
144
- results.push(component.makeInactive());
145
- }
146
- return results;
147
- },
148
- getAttentionTimes: function() {
149
- var component, i, len, obj;
150
- obj = {};
151
- for (i = 0, len = components.length; i < len; i++) {
152
- component = components[i];
153
- if (component.hadAttentionSinceLastGet()) {
154
- // if there are duplicate component names, only the last one survives
155
- obj[component.name] = Math.round(component.getAttentionTime());
156
- }
157
- }
158
- return obj;
159
- },
160
- // visibilityThreshold represents the fraction of each component that must
161
- // be in the viewport before it is considered visible.
162
- // e.g. 0.5 means that half the height or width must be in the viewport
163
- // 1 means that the entire element must be in the viewport
164
- registerComponent: function(name, el, visibilityThreshold, reportingInterval) {
165
- return components.push(new Component(name, el, visibilityThreshold, reportingInterval));
166
- }
167
- };
@@ -1,25 +0,0 @@
1
- // Generated by CoffeeScript 2.7.0
2
- import core from "./core.js";
3
-
4
- import transmit from "./transmit.js";
5
-
6
- import attention from "./attention.js";
7
-
8
- import visibility from "./visibility.js";
9
-
10
- import "./click-path-capture.js";
11
-
12
- import "./perf.js";
13
-
14
- import "./campaign.js";
15
-
16
- core.init("contribution");
17
-
18
- if (window.addEventListener != null) {
19
- attention.init(visibility);
20
- }
21
-
22
- export default {
23
- record: transmit.sendMore,
24
- viewId: transmit.viewId
25
- };