@guardian/ophan-tracker-js 2.1.1 → 2.1.2-next.1
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/NPM-dist/adblock-detection.js +20 -0
- package/NPM-dist/attention.js +126 -0
- package/NPM-dist/campaign.js +23 -0
- package/NPM-dist/click-path-capture.js +68 -0
- package/NPM-dist/components.js +168 -0
- package/{assets → NPM-dist}/contribution.js +4 -13
- package/NPM-dist/core.js +230 -0
- package/{assets → NPM-dist}/embed.js +3 -6
- package/{assets → NPM-dist}/fb-instant.js +2 -5
- package/NPM-dist/heatmap.js +34 -0
- package/{assets → NPM-dist}/holidays.js +3 -6
- package/{assets → NPM-dist}/http-status.js +4 -6
- package/NPM-dist/iframe-tracking.js +80 -0
- package/NPM-dist/interactive.js +17 -0
- package/{assets → NPM-dist}/jobs-courses.js +3 -6
- package/{assets → NPM-dist}/jobs.js +3 -6
- package/NPM-dist/manage-my-account.js +29 -0
- package/{assets → NPM-dist}/membership.js +4 -13
- package/NPM-dist/ng.js +46 -0
- package/NPM-dist/perf.js +36 -0
- package/NPM-dist/privatebrowsing.js +66 -0
- package/{assets → NPM-dist}/r2.js +3 -12
- package/{assets → NPM-dist}/smart-news.js +4 -10
- package/NPM-dist/support.js +28 -0
- package/NPM-dist/tests/click-path-capture.test.js +44 -0
- package/NPM-dist/tests/components.test.js +47 -0
- package/NPM-dist/tests/core.test.js +161 -0
- package/NPM-dist/tests/heatmap.test.js +45 -0
- package/NPM-dist/tests/transmit.test.js +118 -0
- package/NPM-dist/transmit.js +126 -0
- package/NPM-dist/types/abtest.js +1 -0
- package/NPM-dist/types/acquisition.js +1 -0
- package/NPM-dist/types/adblock-detection.d.ts +4 -0
- package/NPM-dist/types/attention.d.ts +9 -0
- package/NPM-dist/types/campaign.d.ts +1 -0
- package/NPM-dist/types/click-path-capture.d.ts +14 -0
- package/NPM-dist/types/component-event.js +1 -0
- package/NPM-dist/types/component-type.js +1 -0
- package/NPM-dist/types/components.d.ts +60 -0
- package/NPM-dist/types/consent.js +1 -0
- package/NPM-dist/types/contribution.d.ts +5 -0
- package/NPM-dist/types/core.d.ts +64 -0
- package/NPM-dist/types/embed.d.ts +5 -0
- package/NPM-dist/types/event.js +1 -0
- package/NPM-dist/types/fb-instant.d.ts +4 -0
- package/NPM-dist/types/heatmap.d.ts +5 -0
- package/NPM-dist/types/holidays.d.ts +5 -0
- package/NPM-dist/types/http-status.d.ts +5 -0
- package/NPM-dist/types/iframe-tracking.d.ts +1 -0
- package/NPM-dist/types/inpageclick.js +1 -0
- package/NPM-dist/types/interaction.js +1 -0
- package/NPM-dist/types/interactive.d.ts +1 -0
- package/NPM-dist/types/jobs-courses.d.ts +5 -0
- package/NPM-dist/types/jobs.d.ts +5 -0
- package/NPM-dist/types/link.js +1 -0
- package/NPM-dist/types/manage-my-account.d.ts +7 -0
- package/NPM-dist/types/media.js +1 -0
- package/NPM-dist/types/membership.d.ts +5 -0
- package/NPM-dist/types/ng.d.ts +26 -0
- package/NPM-dist/types/perf.d.ts +2 -0
- package/NPM-dist/types/platform.js +1 -0
- package/NPM-dist/types/printproduct.js +1 -0
- package/NPM-dist/types/privatebrowsing.d.ts +6 -0
- package/NPM-dist/types/product.js +1 -0
- package/NPM-dist/types/r2.d.ts +4 -0
- package/NPM-dist/types/smart-news.d.ts +5 -0
- package/NPM-dist/types/subscription.js +1 -0
- package/NPM-dist/types/support.d.ts +9 -0
- package/NPM-dist/types/tests/click-path-capture.test.d.ts +1 -0
- package/NPM-dist/types/tests/components.test.d.ts +1 -0
- package/NPM-dist/types/tests/core.test.d.ts +1 -0
- package/NPM-dist/types/tests/heatmap.test.d.ts +1 -0
- package/NPM-dist/types/tests/transmit.test.d.ts +1 -0
- package/NPM-dist/types/transmit.d.ts +17 -0
- package/NPM-dist/types/types/abtest.d.ts +28 -0
- package/NPM-dist/types/types/acquisition.d.ts +116 -0
- package/NPM-dist/types/types/component-event.d.ts +86 -0
- package/NPM-dist/types/types/component-type.d.ts +1 -0
- package/NPM-dist/types/types/consent.d.ts +40 -0
- package/NPM-dist/types/types/event.d.ts +106 -0
- package/NPM-dist/types/types/inpageclick.d.ts +11 -0
- package/NPM-dist/types/types/interaction.d.ts +17 -0
- package/NPM-dist/types/types/link.d.ts +14 -0
- package/NPM-dist/types/types/media.d.ts +98 -0
- package/NPM-dist/types/types/platform.d.ts +10 -0
- package/NPM-dist/types/types/printproduct.d.ts +27 -0
- package/NPM-dist/types/types/product.d.ts +53 -0
- package/NPM-dist/types/types/subscription.d.ts +60 -0
- package/NPM-dist/types/types/utmparameters.d.ts +26 -0
- package/NPM-dist/types/utmparameters.js +1 -0
- package/NPM-dist/types/vendor/adBlockDetectionLib.d.ts +4 -0
- package/NPM-dist/types/visibility.d.ts +5 -0
- package/NPM-dist/types/witness.d.ts +5 -0
- package/NPM-dist/vendor/adBlockDetectionLib.js +312 -0
- package/NPM-dist/visibility.js +22 -0
- package/{assets → NPM-dist}/witness.js +3 -6
- package/package.json +18 -8
- package/readme.md +98 -11
- package/assets/adblock-detection.js +0 -19
- package/assets/attention.js +0 -160
- package/assets/campaign.js +0 -25
- package/assets/click-path-capture.js +0 -80
- package/assets/components.js +0 -199
- package/assets/core.js +0 -282
- package/assets/heatmap.js +0 -53
- package/assets/iframe-tracking.js +0 -88
- package/assets/interactive.js +0 -17
- package/assets/manage-my-account.js +0 -38
- package/assets/ng.js +0 -41
- package/assets/perf.js +0 -40
- package/assets/privatebrowsing.js +0 -71
- package/assets/support.js +0 -34
- package/assets/transmit.js +0 -143
- package/assets/vendor/adBlockDetectionLib.js +0 -366
- package/assets/visibility.js +0 -23
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { _testExports } from '../core.js';
|
|
2
|
+
import transmit from '../transmit';
|
|
3
|
+
const mockSendMore = jest.fn();
|
|
4
|
+
transmit.sendMore = mockSendMore;
|
|
5
|
+
const { getQueryParameterValue, appendComponentDataToEvent, trimComponentNamesToUrlSafeLength, onLoadCapture, onLoadCaptures, processCaptures, appendContentType, } = _testExports;
|
|
6
|
+
describe('appendComponentDataToEvent', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
it('should append unique, trimmed component names to event', () => {
|
|
11
|
+
document.querySelectorAll = jest.fn().mockReturnValue([
|
|
12
|
+
{
|
|
13
|
+
getAttribute: () => 'longComponentNameThatExceedsLimit................................................',
|
|
14
|
+
},
|
|
15
|
+
{ getAttribute: () => 'shortName' },
|
|
16
|
+
{
|
|
17
|
+
getAttribute: () => 'longComponentNameThatExceedsLimit................................................',
|
|
18
|
+
},
|
|
19
|
+
]);
|
|
20
|
+
const event = {};
|
|
21
|
+
appendComponentDataToEvent(event);
|
|
22
|
+
expect(document.querySelectorAll).toHaveBeenCalledWith('[data-component]');
|
|
23
|
+
const expectedTrimmedNames = [
|
|
24
|
+
'longComponentNameThatExceedsLimit.................…',
|
|
25
|
+
'shortName',
|
|
26
|
+
];
|
|
27
|
+
expect(event.renderedComponents).toEqual(expectedTrimmedNames);
|
|
28
|
+
});
|
|
29
|
+
it('should not modify event if no components are found', () => {
|
|
30
|
+
document.querySelectorAll = jest.fn().mockReturnValue([]);
|
|
31
|
+
const event = {};
|
|
32
|
+
appendComponentDataToEvent(event);
|
|
33
|
+
expect(event).not.toHaveProperty('renderedComponents');
|
|
34
|
+
});
|
|
35
|
+
it('should correctly trim component names exceeding maximum length', () => {
|
|
36
|
+
const componentNames = [
|
|
37
|
+
'veryLongComponentName................................................',
|
|
38
|
+
'shortName',
|
|
39
|
+
];
|
|
40
|
+
const trimmedNames = trimComponentNamesToUrlSafeLength(componentNames);
|
|
41
|
+
expect(trimmedNames).toEqual([
|
|
42
|
+
'veryLongComponentName.............................…',
|
|
43
|
+
'shortName',
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('getQueryParameterValue', () => {
|
|
48
|
+
const originalLocation = window.location;
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
delete window.location;
|
|
51
|
+
window.location = { search: '' };
|
|
52
|
+
});
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
window.location = originalLocation;
|
|
55
|
+
});
|
|
56
|
+
it('should return the value of a given query parameter', () => {
|
|
57
|
+
window.location.search = '?param1=value1¶m2=value2';
|
|
58
|
+
const value = getQueryParameterValue('param1');
|
|
59
|
+
expect(value).toBe('value1');
|
|
60
|
+
});
|
|
61
|
+
it('should return undefined if the parameter is not found', () => {
|
|
62
|
+
window.location.search = '?param1=value1¶m2=value2';
|
|
63
|
+
const value = getQueryParameterValue('param3');
|
|
64
|
+
expect(value).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
it('should correctly decode URI-encoded values', () => {
|
|
67
|
+
window.location.search = '?param=encoded%20value';
|
|
68
|
+
const value = getQueryParameterValue('param');
|
|
69
|
+
expect(value).toBe('encoded value');
|
|
70
|
+
});
|
|
71
|
+
it('should handle cases where the query string is empty', () => {
|
|
72
|
+
window.location.search = '';
|
|
73
|
+
const value = getQueryParameterValue('param');
|
|
74
|
+
expect(value).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('onLoadCapture', () => {
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
Object.defineProperty(document, 'readyState', {
|
|
80
|
+
configurable: true,
|
|
81
|
+
get() {
|
|
82
|
+
return this._readyState;
|
|
83
|
+
},
|
|
84
|
+
set(value) {
|
|
85
|
+
this._readyState = value;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
document.readyState = 'loading';
|
|
89
|
+
onLoadCaptures.length = 0;
|
|
90
|
+
jest.clearAllMocks();
|
|
91
|
+
});
|
|
92
|
+
it('should add dataCapture to onLoadCaptures if document is not ready', () => {
|
|
93
|
+
const dataCapture = jest.fn();
|
|
94
|
+
onLoadCapture(dataCapture);
|
|
95
|
+
expect(onLoadCaptures).toContain(dataCapture);
|
|
96
|
+
});
|
|
97
|
+
it('should call dataCapture and sendMore if document is ready', () => {
|
|
98
|
+
document.readyState = 'complete';
|
|
99
|
+
const dataCapture = jest.fn().mockReturnValue({
|
|
100
|
+
performance: {
|
|
101
|
+
dns: 50,
|
|
102
|
+
connection: 100,
|
|
103
|
+
firstByte: 200,
|
|
104
|
+
lastByte: 300,
|
|
105
|
+
domContentLoadedEvent: 400,
|
|
106
|
+
loadEvent: 500,
|
|
107
|
+
navType: 1,
|
|
108
|
+
redirectCount: 2
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
onLoadCapture(dataCapture);
|
|
112
|
+
expect(dataCapture).toHaveBeenCalled();
|
|
113
|
+
expect(transmit.sendMore).toHaveBeenCalledWith({
|
|
114
|
+
performance: {
|
|
115
|
+
dns: 50,
|
|
116
|
+
connection: 100,
|
|
117
|
+
firstByte: 200,
|
|
118
|
+
lastByte: 300,
|
|
119
|
+
domContentLoadedEvent: 400,
|
|
120
|
+
loadEvent: 500,
|
|
121
|
+
navType: 1,
|
|
122
|
+
redirectCount: 2
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe('processCaptures', () => {
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
onLoadCaptures.length = 0;
|
|
130
|
+
jest.clearAllMocks();
|
|
131
|
+
});
|
|
132
|
+
it('should call all dataCaptures and merge their output into the sendMore function', () => {
|
|
133
|
+
onLoadCaptures.push(() => ({ performance: { domContentLoadedEvent: 400, loadEvent: 500 } }));
|
|
134
|
+
processCaptures();
|
|
135
|
+
expect(transmit.sendMore).toHaveBeenCalledWith({
|
|
136
|
+
performance: {
|
|
137
|
+
domContentLoadedEvent: 400,
|
|
138
|
+
loadEvent: 500
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('appendContentType', () => {
|
|
144
|
+
it('should set the lowercase content type for next-gen platform', () => {
|
|
145
|
+
const event = {};
|
|
146
|
+
const guardian = { config: { page: { contentType: 'Article' } } };
|
|
147
|
+
appendContentType(event, 'next-gen', guardian);
|
|
148
|
+
expect(event.contentType).toBe('article');
|
|
149
|
+
});
|
|
150
|
+
it('should set the lowercase content type for other platforms', () => {
|
|
151
|
+
const event = {};
|
|
152
|
+
const guardian = { page: { contentTypes: 'LiveBlog' } };
|
|
153
|
+
appendContentType(event, 'other', guardian);
|
|
154
|
+
expect(event.contentType).toBe('liveblog');
|
|
155
|
+
});
|
|
156
|
+
it('should not set content type if it is not available', () => {
|
|
157
|
+
const event = {};
|
|
158
|
+
appendContentType(event, 'next-gen', {});
|
|
159
|
+
expect(event.contentType).toBeUndefined();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
describe('Heatmap init', () => {
|
|
3
|
+
beforeEach(() => {
|
|
4
|
+
jest.resetModules();
|
|
5
|
+
});
|
|
6
|
+
it('should load the heatmap based on a query param', () => {
|
|
7
|
+
const heatmap = require('../heatmap.js').default;
|
|
8
|
+
const scriptSrc = '//dashboard.ophan.co.uk/assets/js/heatmap-bookmarklet.js';
|
|
9
|
+
Object.defineProperty(window, 'location', {
|
|
10
|
+
value: new URL('https://www.theguardian.com/uk?heatmap=show'),
|
|
11
|
+
writable: true,
|
|
12
|
+
});
|
|
13
|
+
heatmap.init();
|
|
14
|
+
const scripts = document.getElementsByTagName("script");
|
|
15
|
+
let heatmapScript = Array.from(scripts).find(item => item.src.endsWith(scriptSrc));
|
|
16
|
+
expect(heatmapScript).not.toBeNull();
|
|
17
|
+
});
|
|
18
|
+
it('should load the heatmap script once', () => {
|
|
19
|
+
const heatmap = require('../heatmap.js').default;
|
|
20
|
+
const scriptSrc = '//dashboard.ophan.co.uk/assets/js/react/heatphan/index.js';
|
|
21
|
+
Object.defineProperty(window, 'location', {
|
|
22
|
+
value: new URL('https://www.theguardian.com/uk?heatmap=experimental'),
|
|
23
|
+
writable: true,
|
|
24
|
+
});
|
|
25
|
+
for (let i = 0; i < 10; i++) {
|
|
26
|
+
heatmap.init();
|
|
27
|
+
}
|
|
28
|
+
const scripts = document.getElementsByTagName("script");
|
|
29
|
+
let heatmapScript = Array.from(scripts).filter(item => item.src.endsWith(scriptSrc));
|
|
30
|
+
expect(heatmapScript.length).toBe(1);
|
|
31
|
+
expect(heatmapScript[0]).not.toBeNull();
|
|
32
|
+
});
|
|
33
|
+
it('should load heatphan based on a query param', () => {
|
|
34
|
+
const heatmap = require('../heatmap.js').default;
|
|
35
|
+
const scriptSrc = '//dashboard.ophan.co.uk/assets/js/react/heatphan/index.js';
|
|
36
|
+
Object.defineProperty(window, 'location', {
|
|
37
|
+
value: new URL('https://www.theguardian.com/uk?heatmap=experimental'),
|
|
38
|
+
writable: true,
|
|
39
|
+
});
|
|
40
|
+
heatmap.init();
|
|
41
|
+
const scripts = document.getElementsByTagName("script");
|
|
42
|
+
let heatmapScript = Array.from(scripts).find(item => item.src.endsWith(scriptSrc));
|
|
43
|
+
expect(heatmapScript).not.toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
global.ophanRemoteHost = 'https://ophan.theguardian.com';
|
|
2
|
+
global.fetch = jest.fn(() => Promise.resolve({
|
|
3
|
+
json: () => Promise.resolve({}),
|
|
4
|
+
}));
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
fetch.mockClear();
|
|
7
|
+
});
|
|
8
|
+
import transmit, { _testExports } from "../transmit";
|
|
9
|
+
const { smartEncode, isDefined, buildQueryString, generatePageViewId } = _testExports;
|
|
10
|
+
describe('generatePageViewId', () => {
|
|
11
|
+
it('generates a viewId of correct length', () => {
|
|
12
|
+
const viewId = generatePageViewId();
|
|
13
|
+
expect(typeof viewId).toBe('string');
|
|
14
|
+
expect(viewId.length).toBe(20);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('smartEncode', () => {
|
|
18
|
+
it('encodes strings correctly', () => {
|
|
19
|
+
expect(smartEncode('test string')).toBe(encodeURIComponent('test string'));
|
|
20
|
+
});
|
|
21
|
+
it('encodes numbers correctly', () => {
|
|
22
|
+
expect(smartEncode(123)).toBe(encodeURIComponent(123));
|
|
23
|
+
});
|
|
24
|
+
it('encodes objects correctly', () => {
|
|
25
|
+
const obj = { a: 1, b: 'test' };
|
|
26
|
+
expect(smartEncode(obj)).toBe(encodeURIComponent(JSON.stringify(obj)));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('isDefined', () => {
|
|
30
|
+
it('returns true for non-null values', () => {
|
|
31
|
+
expect(isDefined(123)).toBe(true);
|
|
32
|
+
expect(isDefined('test')).toBe(true);
|
|
33
|
+
expect(isDefined([1])).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('returns false for null or undefined values', () => {
|
|
36
|
+
expect(isDefined(null)).toBeFalsy();
|
|
37
|
+
expect(isDefined(undefined)).toBeFalsy();
|
|
38
|
+
});
|
|
39
|
+
it('returns false for empty arrays', () => {
|
|
40
|
+
expect(isDefined([])).toBeFalsy();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('buildQueryString', () => {
|
|
44
|
+
it('should build a query string from an object', () => {
|
|
45
|
+
const obj = {
|
|
46
|
+
"v": "17",
|
|
47
|
+
"platform": "next-gen",
|
|
48
|
+
"url": "https://www.theguardian.com/commentisfree/2024/jan/04/luke-littler-darts-championship-pdc-british-sport",
|
|
49
|
+
"ref": "",
|
|
50
|
+
"visibilityState": "visible",
|
|
51
|
+
"tz": "0",
|
|
52
|
+
"navigationType": "reload",
|
|
53
|
+
"contentType": "article",
|
|
54
|
+
"viewId": "lqz6au1wqb9mwq92w4zh",
|
|
55
|
+
};
|
|
56
|
+
const queryString = buildQueryString(obj);
|
|
57
|
+
expect(queryString).toContain(`v=17&platform=next-gen&url=https%3A%2F%2Fwww.theguardian.com%2Fcommentisfree%2F2024%2Fjan%2F04%2Fluke-littler-darts-championship-pdc-british-sport&ref=&visibilityState=visible&tz=0&navigationType=reload&contentType=article`);
|
|
58
|
+
});
|
|
59
|
+
it('should ignore null and undefined values', () => {
|
|
60
|
+
const obj = {
|
|
61
|
+
"v": "17",
|
|
62
|
+
"platform": "next-gen",
|
|
63
|
+
"url": "https://www.theguardian.com/commentisfree/2024/jan/04/luke-littler-darts-championship-pdc-british-sport",
|
|
64
|
+
"ref": "",
|
|
65
|
+
"visibilityState": "visible",
|
|
66
|
+
"tz": "0",
|
|
67
|
+
"navigationType": "reload",
|
|
68
|
+
"contentType": "article",
|
|
69
|
+
"viewId": "lqz6au1wqb9mwq92w4zh",
|
|
70
|
+
"nullValue": null
|
|
71
|
+
};
|
|
72
|
+
const queryString = buildQueryString(obj);
|
|
73
|
+
expect(queryString).toContain(`v=17&platform=next-gen&url=https%3A%2F%2Fwww.theguardian.com%2Fcommentisfree%2F2024%2Fjan%2F04%2Fluke-littler-darts-championship-pdc-british-sport&ref=&visibilityState=visible&tz=0&navigationType=reload&contentType=article`);
|
|
74
|
+
expect(queryString).not.toContain(`nullValue`);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('sendMore', () => {
|
|
78
|
+
it('should call send with correct URL and callback', async () => {
|
|
79
|
+
const obj = {
|
|
80
|
+
"componentEvent": {
|
|
81
|
+
"component": {
|
|
82
|
+
"componentType": "ACQUISITIONS_ENGAGEMENT_BANNER",
|
|
83
|
+
"products": [],
|
|
84
|
+
"campaignCode": "2023-12-19_EOY_ANNUAL_PUSH__BANNER_V2",
|
|
85
|
+
"id": "designable-banner : close",
|
|
86
|
+
"labels": []
|
|
87
|
+
},
|
|
88
|
+
"abTest": {
|
|
89
|
+
"name": "2023-12-19_EOY_ANNUAL_PUSH__BANNER",
|
|
90
|
+
"variant": "V2"
|
|
91
|
+
},
|
|
92
|
+
"action": "CLICK"
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const callback = jest.fn();
|
|
96
|
+
await transmit.sendMore(obj, callback);
|
|
97
|
+
// Access the first argument (the URL) of the first call to fetch
|
|
98
|
+
const calledUrl = fetch.mock.calls[0][0];
|
|
99
|
+
expect(calledUrl).toContain('ACQUISITIONS_ENGAGEMENT_BANNER');
|
|
100
|
+
expect(calledUrl).toContain('2023-12-19_EOY_ANNUAL_PUSH__BANNER_V2');
|
|
101
|
+
expect(calledUrl).toContain('CLICK');
|
|
102
|
+
expect(callback).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
it('should not call send if object has no defined properties', () => {
|
|
105
|
+
const obj = { key1: undefined, key2: null };
|
|
106
|
+
const callback = jest.fn();
|
|
107
|
+
transmit.sendMore(obj, callback);
|
|
108
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
109
|
+
expect(callback).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
it('should not call send if object is empty', () => {
|
|
112
|
+
const obj = {};
|
|
113
|
+
const callback = jest.fn();
|
|
114
|
+
transmit.sendMore(obj, callback);
|
|
115
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
116
|
+
expect(callback).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const ophanRemoteHost = window?.ophanRemoteHost || 'https://ophan.theguardian.com';
|
|
2
|
+
/**
|
|
3
|
+
* Generates a unique page view ID.
|
|
4
|
+
* @returns {string}
|
|
5
|
+
*/
|
|
6
|
+
const generatePageViewId = () => {
|
|
7
|
+
return `${new Date().getTime().toString(36)}${'xxxxxxxxxxxx'.replace(/x/g, () => Math.floor(Math.random() * 36).toString(36))}`;
|
|
8
|
+
};
|
|
9
|
+
// Guardian Frontend Project pre-generates the pageViewId
|
|
10
|
+
let viewId;
|
|
11
|
+
if (typeof guardian !== 'undefined') {
|
|
12
|
+
viewId = guardian.config?.ophan?.pageViewId ?? generatePageViewId();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
viewId = generatePageViewId();
|
|
16
|
+
}
|
|
17
|
+
const bumpViewId = () => {
|
|
18
|
+
viewId = generatePageViewId();
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Sends the initial event associated with this page view.
|
|
22
|
+
* @param {Record<string, any>} obj - The data to be sent.
|
|
23
|
+
*/
|
|
24
|
+
const sendInitial = (obj) => {
|
|
25
|
+
send(`${ophanRemoteHost}/img/1?${buildQueryString(obj)}`);
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Encodes a value for URL transmission.
|
|
29
|
+
* @param {string | number} value - The value to encode.
|
|
30
|
+
* @returns {string} The encoded value.
|
|
31
|
+
*/
|
|
32
|
+
const smartEncode = (value) => {
|
|
33
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
34
|
+
return encodeURIComponent(value);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return encodeURIComponent(JSON.stringify(value));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Used for sending additional events after the initial PageView event.
|
|
42
|
+
* @param {EventPayload} obj - The data object to be sent.
|
|
43
|
+
* @param {Function} [f] - An optional callback function.
|
|
44
|
+
*/
|
|
45
|
+
const sendMore = (obj, f) => {
|
|
46
|
+
if (!JSON)
|
|
47
|
+
return;
|
|
48
|
+
const values = [];
|
|
49
|
+
for (const prop in obj) {
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
if (obj.hasOwnProperty(prop) && isDefined(obj[prop])) {
|
|
52
|
+
// @ts-ignore
|
|
53
|
+
values.push(`${encodeURIComponent(prop)}=${smartEncode(obj[prop])}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (values.length === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const url = `${ophanRemoteHost}/img/2?viewId=${viewId}&${values.join('&')}`;
|
|
60
|
+
send(url, f);
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Checks if a value is defined and not null.
|
|
64
|
+
* @param {*} v - The value to check.
|
|
65
|
+
* @returns {boolean} True if the value is defined, false otherwise.
|
|
66
|
+
*/
|
|
67
|
+
const isDefined = (v) => {
|
|
68
|
+
if (Array.isArray(v)) {
|
|
69
|
+
return !!v.length;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return v != null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Builds a query string from an object.
|
|
77
|
+
* @param {Record<string, any>} obj - The object to convert to a query string.
|
|
78
|
+
* @returns {string} The resulting query string.
|
|
79
|
+
*/
|
|
80
|
+
const buildQueryString = (obj) => {
|
|
81
|
+
obj.viewId = viewId;
|
|
82
|
+
const values = [];
|
|
83
|
+
for (const prop in obj) {
|
|
84
|
+
if (obj.hasOwnProperty(prop) && obj[prop] != null) {
|
|
85
|
+
values.push(`${encodeURIComponent(prop)}=${encodeURIComponent(obj[prop])}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return values.join('&');
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Sends data to a specified URL.
|
|
92
|
+
* @param {string} url - The URL to send data to.
|
|
93
|
+
* @param {() => void} [f] - An optional callback function.
|
|
94
|
+
*/
|
|
95
|
+
const send = (url, f) => {
|
|
96
|
+
if (typeof fetch === 'function') {
|
|
97
|
+
fetch(url, { method: 'GET', mode: 'no-cors', keepalive: true, credentials: 'include' }).then(() => f?.());
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const image = new Image();
|
|
101
|
+
const checker = setInterval(() => {
|
|
102
|
+
if (image.complete) {
|
|
103
|
+
if (typeof f === 'function') {
|
|
104
|
+
f();
|
|
105
|
+
}
|
|
106
|
+
clearInterval(checker);
|
|
107
|
+
}
|
|
108
|
+
}, 10);
|
|
109
|
+
image.src = url;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
export default {
|
|
113
|
+
sendInitial,
|
|
114
|
+
sendMore,
|
|
115
|
+
viewId,
|
|
116
|
+
bumpViewId,
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Exports for testing purposes.
|
|
120
|
+
*/
|
|
121
|
+
export const _testExports = {
|
|
122
|
+
generatePageViewId,
|
|
123
|
+
smartEncode,
|
|
124
|
+
isDefined,
|
|
125
|
+
buildQueryString,
|
|
126
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare namespace _default {
|
|
2
|
+
export { initAttention as init };
|
|
3
|
+
export { initComponent };
|
|
4
|
+
export { setEventEmitter };
|
|
5
|
+
}
|
|
6
|
+
export default _default;
|
|
7
|
+
declare function initAttention(visibility: any): void;
|
|
8
|
+
declare function initComponent(name: any, el: any, visibilityThreshold?: number): void;
|
|
9
|
+
declare function setEventEmitter(emitter: any): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function validAncestorAnchorElement(el: Element): Element | null;
|
|
2
|
+
export function getContainingComponent(el: Element): string | null;
|
|
3
|
+
declare namespace _default {
|
|
4
|
+
export { getDataLinkNames };
|
|
5
|
+
}
|
|
6
|
+
export default _default;
|
|
7
|
+
/**
|
|
8
|
+
* Collects all `data-link-name` attribute values from the ancestor elements.
|
|
9
|
+
*
|
|
10
|
+
* @param {Element} el - The element to start searching from.
|
|
11
|
+
* @param {string[]} dataLinkNames - Initial array of data link names (optional).
|
|
12
|
+
* @returns {string[]} Array of `data-link-name` values.
|
|
13
|
+
*/
|
|
14
|
+
declare function getDataLinkNames(el: Element, dataLinkNames?: string[]): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @type {Component[]}
|
|
3
|
+
*/
|
|
4
|
+
export const components: Component[];
|
|
5
|
+
declare namespace _default {
|
|
6
|
+
export function setEventEmitter(emitter: Object): Object;
|
|
7
|
+
export { startMonitoring };
|
|
8
|
+
export { stopMonitoring };
|
|
9
|
+
export { getAttentionTimes };
|
|
10
|
+
export let registerComponent: (...args: [name: string, element: HTMLElement, visibilityThreshold1: number, reportingInterval1: number]) => void;
|
|
11
|
+
}
|
|
12
|
+
export default _default;
|
|
13
|
+
declare class Component {
|
|
14
|
+
/**
|
|
15
|
+
* Component constructor.
|
|
16
|
+
* @param {string} name - The name of the component.
|
|
17
|
+
* @param {HTMLElement} element - The DOM element of the component.
|
|
18
|
+
* @param {number} visibilityThreshold1 -
|
|
19
|
+
* visibilityThreshold represents the fraction of each component that must
|
|
20
|
+
* be in the viewport before it is considered visible.
|
|
21
|
+
* e.g. 0.5 means that half the height or width must be in the viewport
|
|
22
|
+
* 1 means that the entire element must be in the viewport
|
|
23
|
+
* @param {number} reportingInterval1 - ??
|
|
24
|
+
*/
|
|
25
|
+
constructor(name: string, element: HTMLElement, visibilityThreshold1: number, reportingInterval1: number);
|
|
26
|
+
name: string;
|
|
27
|
+
element: HTMLElement;
|
|
28
|
+
visibilityThreshold: number;
|
|
29
|
+
reportingInterval: number;
|
|
30
|
+
visible: boolean;
|
|
31
|
+
totalAttentionMs: number;
|
|
32
|
+
unrecordedAttentionStarted: number | null;
|
|
33
|
+
reportedTotalAttentionMs: number;
|
|
34
|
+
usingEmitter: boolean;
|
|
35
|
+
visCheck: () => number | null | undefined;
|
|
36
|
+
isVisible(threshold?: number): boolean;
|
|
37
|
+
visibilityHasChanged(): boolean;
|
|
38
|
+
rebindToEventEmitter(): any;
|
|
39
|
+
checkVisibility(): number | null | undefined;
|
|
40
|
+
makeActive(): number;
|
|
41
|
+
makeInactive(): null;
|
|
42
|
+
hadAttentionSinceLastGet(): boolean;
|
|
43
|
+
getAttentionTime(): number;
|
|
44
|
+
incrementTotalAttentionTimeByUnrecordedAmount(): number | undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Begins monitoring the components on the page for attention time.
|
|
48
|
+
* @returns {Array} Array of results from makeActive() for each visible component.
|
|
49
|
+
*/
|
|
50
|
+
declare function startMonitoring(): any[];
|
|
51
|
+
/**
|
|
52
|
+
* Stops monitoring the components on the page for attention time.
|
|
53
|
+
* @returns {Array} Array of results from makeInactive() for each component.
|
|
54
|
+
*/
|
|
55
|
+
declare function stopMonitoring(): any[];
|
|
56
|
+
/**
|
|
57
|
+
* Retrieves the attention times for all components.
|
|
58
|
+
* @returns {Object}
|
|
59
|
+
*/
|
|
60
|
+
declare function getAttentionTimes(): Object;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
declare namespace _default {
|
|
2
|
+
export { init };
|
|
3
|
+
export { storeDataToSendOnNextEvent };
|
|
4
|
+
export { onLoadCapture };
|
|
5
|
+
export function servingPlatform(): any;
|
|
6
|
+
export let viewId: string;
|
|
7
|
+
export { sendInitialEvent };
|
|
8
|
+
}
|
|
9
|
+
export default _default;
|
|
10
|
+
export namespace _testExports {
|
|
11
|
+
export { appendComponentDataToEvent };
|
|
12
|
+
export { trimComponentNamesToUrlSafeLength };
|
|
13
|
+
export { getQueryParameterValue };
|
|
14
|
+
export { onLoadCaptures };
|
|
15
|
+
export { onLoadCapture };
|
|
16
|
+
export { processCaptures };
|
|
17
|
+
export { appendContentType };
|
|
18
|
+
}
|
|
19
|
+
declare let init: any;
|
|
20
|
+
declare let storeDataToSendOnNextEvent: any;
|
|
21
|
+
/**
|
|
22
|
+
* Registers a data capture function to be called when the document is fully loaded.
|
|
23
|
+
* The result of this function is then sent to the tracker backend service.
|
|
24
|
+
* If the document is already loaded, this is done immediately.
|
|
25
|
+
*
|
|
26
|
+
* @param {() => Record<string, unknown>} dataCapture - A function that returns an object containing performance metrics such as page load time, network latency, etc.
|
|
27
|
+
* Refer to the `perf.js` file where this function is used for more info.
|
|
28
|
+
*/
|
|
29
|
+
declare function onLoadCapture(dataCapture: () => Record<string, unknown>): void;
|
|
30
|
+
declare let sendInitialEvent: any;
|
|
31
|
+
/**
|
|
32
|
+
* Appends data about rendered components to an event object.
|
|
33
|
+
* Extracts component names from elements with a 'data-component' attribute, trims them for URL safety, ensures uniqueness, and adds them to the event.
|
|
34
|
+
* @param {Object} event - The event object to which component data will be appended.
|
|
35
|
+
*/
|
|
36
|
+
declare function appendComponentDataToEvent(event: Object): void;
|
|
37
|
+
/**
|
|
38
|
+
* Trims component names to a maximum length to ensure they are URL safe.
|
|
39
|
+
* If a component name exceeds the maximum length, it is truncated and appended with an ellipsis.
|
|
40
|
+
* @param {string[]} componentNames - An array of component names.
|
|
41
|
+
* @returns {string[]} An array of trimmed component names.
|
|
42
|
+
*/
|
|
43
|
+
declare function trimComponentNamesToUrlSafeLength(componentNames: string[]): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Retrieves the value of a specified query parameter from the current page's URL.
|
|
46
|
+
* Using an old-fashioned way of finding a query param rather than using searchParams
|
|
47
|
+
* as we don't control which environments the CDN distribution is being used in and so don't know if it's supported.
|
|
48
|
+
* @param {string} paramName - The name of the query parameter to retrieve.
|
|
49
|
+
* @returns {string | undefined} The value of the query parameter, or undefined if not found.
|
|
50
|
+
*/
|
|
51
|
+
declare function getQueryParameterValue(paramName: string): string | undefined;
|
|
52
|
+
declare const onLoadCaptures: any[];
|
|
53
|
+
/**
|
|
54
|
+
* Processes all registered data capture functions, merges their outputs and sends it to the tracker backend service.
|
|
55
|
+
* If on a 'next-gen' platform, additional component data is appended to the event via the `appendComponentDataToEvent` function.
|
|
56
|
+
*/
|
|
57
|
+
declare function processCaptures(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Determines the 'content type' based on the platform and guardian object.
|
|
60
|
+
* @param {Record<string, unknown>} event - The event object that will be updated with contentType.
|
|
61
|
+
* @param {string} platform - The current platform ('next-gen', 'embed' or other).
|
|
62
|
+
* @param {Record<string, unknown>} guardian - The guardian object.
|
|
63
|
+
*/
|
|
64
|
+
declare function appendContentType(event: Record<string, unknown>, platform: string, guardian: Record<string, unknown>): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|