@schibsted/pulse-tracker-proxy 1.0.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/dist/cjs/consts.js +19 -0
- package/dist/cjs/get-proxy.js +93 -0
- package/dist/cjs/index.js +17 -0
- package/dist/cjs/load-autotracker.js +34 -0
- package/dist/cjs/send-message.js +64 -0
- package/dist/cjs/tracker-proxy.js +72 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/version.json +1 -0
- package/dist/ejs/consts.js +15 -0
- package/dist/ejs/get-proxy.js +90 -0
- package/dist/ejs/index.js +1 -0
- package/dist/ejs/load-autotracker.js +31 -0
- package/dist/ejs/send-message.js +61 -0
- package/dist/ejs/tracker-proxy.js +65 -0
- package/dist/ejs/types.js +1 -0
- package/dist/ejs/version.json +1 -0
- package/dist/index.d.ts +5965 -0
- package/package.json +33 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ALLOWED_METHODS = exports.MESSAGE_TYPE_RETURN_ERROR = exports.MESSAGE_TYPE_RETURN = exports.MESSAGE_TYPE_INVOKE = exports.MESSAGE_TYPE_ACK = exports.MESSAGE_TYPE_REQUEST = exports.MESSAGE_PREFIX = void 0;
|
|
4
|
+
exports.isPulseProxyMessage = isPulseProxyMessage;
|
|
5
|
+
// Sync the content between files below this line:
|
|
6
|
+
exports.MESSAGE_PREFIX = 'pulse-tracker-proxy';
|
|
7
|
+
exports.MESSAGE_TYPE_REQUEST = `${exports.MESSAGE_PREFIX}:request`;
|
|
8
|
+
exports.MESSAGE_TYPE_ACK = `${exports.MESSAGE_PREFIX}:ack`;
|
|
9
|
+
exports.MESSAGE_TYPE_INVOKE = `${exports.MESSAGE_PREFIX}:invoke`;
|
|
10
|
+
exports.MESSAGE_TYPE_RETURN = `${exports.MESSAGE_PREFIX}:return`;
|
|
11
|
+
exports.MESSAGE_TYPE_RETURN_ERROR = `${exports.MESSAGE_PREFIX}:return-error`;
|
|
12
|
+
exports.ALLOWED_METHODS = ['track'];
|
|
13
|
+
function isPulseProxyMessage(message) {
|
|
14
|
+
return (message !== null &&
|
|
15
|
+
typeof message === 'object' &&
|
|
16
|
+
'type' in message &&
|
|
17
|
+
typeof message.type === 'string' &&
|
|
18
|
+
message.type.startsWith(exports.MESSAGE_PREFIX));
|
|
19
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPulseTrackerProxy = getPulseTrackerProxy;
|
|
4
|
+
const consts_1 = require("./consts");
|
|
5
|
+
const load_autotracker_1 = require("./load-autotracker");
|
|
6
|
+
const send_message_1 = require("./send-message");
|
|
7
|
+
const tracker_proxy_1 = require("./tracker-proxy");
|
|
8
|
+
/**
|
|
9
|
+
* Pulse Tracker Proxy System Architecture
|
|
10
|
+
* --------------------------------------
|
|
11
|
+
* The proxy system allows for cross-frame/cross-window tracking events without directly exposing
|
|
12
|
+
* the tracking API to child frames. It works in two modes:
|
|
13
|
+
*
|
|
14
|
+
* 1. Mobile App Webview Mode:
|
|
15
|
+
* - Detects if running in a mobile app webview by checking for `pulseEventHandler`
|
|
16
|
+
* - Loads the Pulse autotracker with the specified version and provider
|
|
17
|
+
* - Directly communicates with the native app's tracking system
|
|
18
|
+
*
|
|
19
|
+
* 2. Iframe/Cross-window Mode:
|
|
20
|
+
* - Establishes a connection with the parent window (or specified target)
|
|
21
|
+
* - Uses postMessage for communication
|
|
22
|
+
* - Handles timeouts, retries, and error cases for reliable messaging
|
|
23
|
+
* - Proxies tracking calls to the parent window which can then handle them
|
|
24
|
+
*
|
|
25
|
+
* The system adds a proxy version to all events to help with debugging and tracking.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Establishes a connection with the target frame and returns a pulse tracker proxy object.
|
|
29
|
+
* This function automatically detects whether it's running in a mobile app webview or
|
|
30
|
+
* needs to use iframe/cross-window communication.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const tracker = await getPulseTrackerProxy({
|
|
35
|
+
* provider: 'my-provider',
|
|
36
|
+
* debug: console.log
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* tracker.track('trackerEvent', { ... });
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @param options - Configuration options for the pulse tracker proxy
|
|
43
|
+
* @returns A promise that resolves to a proxy object implementing the Pulse tracker interface
|
|
44
|
+
* @throws Error if connection fails, timeout occurs, or required options are missing
|
|
45
|
+
*/
|
|
46
|
+
function getPulseTrackerProxy({ provider, autotrackerVersion = '3', window = globalThis.window, target = window.parent, timeout = 5000, retries = 3, retryTimeout = 1000, debug = () => { }, } = {}) {
|
|
47
|
+
debug('[Pulse Tracker Proxy] Checking if `pulseEventHandler` is provided in the current window...');
|
|
48
|
+
if (window.pulseEventHandler) {
|
|
49
|
+
debug(`[Pulse Tracker Proxy] \`pulseEventHandler\` found in the current window. Assuming it is the app webview. Loading autotracker version ${autotrackerVersion}...`);
|
|
50
|
+
if (!provider) {
|
|
51
|
+
debug('[Pulse Tracker Proxy] `provider` is not set - cannot create pulse tracker.');
|
|
52
|
+
return Promise.reject('`provider` is not set - cannot create pulse tracker.');
|
|
53
|
+
}
|
|
54
|
+
return (0, load_autotracker_1.loadAutotracker)(window, provider, autotrackerVersion);
|
|
55
|
+
}
|
|
56
|
+
debug('[Pulse Tracker Proxy] `pulseEventHandler` not found in the current window - not a mobile app webview.');
|
|
57
|
+
debug('[Pulse Tracker Proxy] Trying to establish connection...', { target, timeout, retries });
|
|
58
|
+
if (!target) {
|
|
59
|
+
return Promise.reject(new Error('Target window not provided. Possibly window.parent is not available.'));
|
|
60
|
+
}
|
|
61
|
+
let ackTimeout;
|
|
62
|
+
let alreadyTimedOut = false;
|
|
63
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
64
|
+
ackTimeout = setTimeout(() => {
|
|
65
|
+
alreadyTimedOut = true;
|
|
66
|
+
debug('[Pulse Tracker Proxy] Ack timeout. Sorry, target seems to not support the proxy.');
|
|
67
|
+
reject(new Error(`Pulse tracker proxy not found within ${timeout} ms`));
|
|
68
|
+
}, timeout);
|
|
69
|
+
});
|
|
70
|
+
const connectionPromise = (async () => {
|
|
71
|
+
for (let i = 0; i < retries; i++) {
|
|
72
|
+
if (alreadyTimedOut) {
|
|
73
|
+
debug('[Pulse Tracker Proxy] Configured timeout has passed before the next attempt');
|
|
74
|
+
throw new Error('Configured timeout has passed before the next attempt.');
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
debug(`[Pulse Tracker Proxy] Sending request to target window (attempt: ${i + 1})...`);
|
|
78
|
+
await (0, send_message_1.sendMessage)({ type: consts_1.MESSAGE_TYPE_REQUEST }, { target, timeout: retryTimeout });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
debug(`[Pulse Tracker Proxy] Ack Request failed: ${error}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
clearTimeout(ackTimeout);
|
|
85
|
+
debug(`[Pulse Tracker Proxy] Ack Request successful (attempt: ${i + 1})`);
|
|
86
|
+
return new tracker_proxy_1.PulseTrackerProxyImpl(target, debug);
|
|
87
|
+
}
|
|
88
|
+
clearTimeout(ackTimeout);
|
|
89
|
+
debug('[Pulse Tracker Proxy] Retries exhausted. Target did not respond. Failing.');
|
|
90
|
+
throw new Error('Failed to establish connection with pulse tracker proxy');
|
|
91
|
+
})();
|
|
92
|
+
return Promise.race([timeoutPromise, connectionPromise]);
|
|
93
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./get-proxy"), exports);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadAutotracker = loadAutotracker;
|
|
4
|
+
/**
|
|
5
|
+
* Loads the Pulse autotracker script onto the page and initializes it with the provided configuration.
|
|
6
|
+
*
|
|
7
|
+
* @param window - The browser window object to attach the script to
|
|
8
|
+
* @param provider - The provider ID to initialize the Pulse tracker with
|
|
9
|
+
* @param version - The version of the Pulse autotracker to load
|
|
10
|
+
* @returns A promise that resolves to the configured Tracker instance
|
|
11
|
+
* @throws Error if script loading fails or Pulse doesn't initialize correctly
|
|
12
|
+
*/
|
|
13
|
+
async function loadAutotracker(window, provider, version) {
|
|
14
|
+
await new Promise((resolve, reject) => {
|
|
15
|
+
const element = window.document.createElement('script');
|
|
16
|
+
element.src = `//sdk.pulse.schibsted.com/versioned/${version}/pulse.min.js`;
|
|
17
|
+
element.async = true;
|
|
18
|
+
element.onload = resolve;
|
|
19
|
+
element.onerror = () => {
|
|
20
|
+
reject(new Error(`Failed to load autotracker script from src: ${element.src}`));
|
|
21
|
+
};
|
|
22
|
+
window.document.head.appendChild(element);
|
|
23
|
+
});
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
if (!window.pulse) {
|
|
26
|
+
reject(new Error('Pulse autotracker did not load correctly!'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
window.pulse('init', provider);
|
|
30
|
+
window.pulse((tracker) => {
|
|
31
|
+
resolve(tracker);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendMessage = sendMessage;
|
|
4
|
+
const consts_1 = require("./consts");
|
|
5
|
+
/**
|
|
6
|
+
* Sends a message to a target window and returns a promise that resolves with the response.
|
|
7
|
+
*
|
|
8
|
+
* This function creates a MessageChannel to facilitate two-way communication with the target.
|
|
9
|
+
* It handles timeouts and error responses appropriately.
|
|
10
|
+
*
|
|
11
|
+
* @param message - The message object to send to the target window
|
|
12
|
+
* @param options - Configuration options for sending the message
|
|
13
|
+
* @param options.target - The target window to send the message to (e.g., iframe.contentWindow)
|
|
14
|
+
* @param options.timeout - Optional timeout in milliseconds after which the promise will reject
|
|
15
|
+
* @param options.debug - Optional debug function for logging message events
|
|
16
|
+
* @returns A promise that resolves with the response message or rejects with an error
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { sendMessage } from './send-message';
|
|
21
|
+
*
|
|
22
|
+
* const message = { type: 'SOME_MESSAGE_TYPE', payload: { ... } };
|
|
23
|
+
* sendMessage(message, {
|
|
24
|
+
* target: document.querySelector('iframe').contentWindow,
|
|
25
|
+
* timeout: 5000,
|
|
26
|
+
* debug: console.log
|
|
27
|
+
* }).then(response => {
|
|
28
|
+
* // Handle response
|
|
29
|
+
* }).catch(error => {
|
|
30
|
+
* // Handle error
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
function sendMessage(message, { target, timeout, debug = () => { } }) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const channel = new MessageChannel();
|
|
37
|
+
const requestTimeout = timeout
|
|
38
|
+
? setTimeout(() => {
|
|
39
|
+
channel.port1.close();
|
|
40
|
+
reject(new Error(`Message of type ${message.type} timed out`));
|
|
41
|
+
}, timeout)
|
|
42
|
+
: undefined;
|
|
43
|
+
const responseHandler = (event) => {
|
|
44
|
+
debug('[Pulse Tracker Proxy] Received the response on the MessageChannel', event);
|
|
45
|
+
if (!(0, consts_1.isPulseProxyMessage)(event.data)) {
|
|
46
|
+
debug('[Pulse Tracker Proxy] Response was not a valid Proxy Message', event.data);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (event.data.type === consts_1.MESSAGE_TYPE_RETURN_ERROR) {
|
|
50
|
+
debug('[Pulse Tracker Proxy] Received an return error response', event.data.error);
|
|
51
|
+
clearTimeout(requestTimeout);
|
|
52
|
+
channel.port1.close();
|
|
53
|
+
reject(new Error(event.data.error));
|
|
54
|
+
}
|
|
55
|
+
debug('[Pulse Tracker Proxy] Received a valid response', event.data);
|
|
56
|
+
clearTimeout(requestTimeout);
|
|
57
|
+
channel.port1.close();
|
|
58
|
+
resolve(event.data);
|
|
59
|
+
};
|
|
60
|
+
channel.port1.onmessage = responseHandler;
|
|
61
|
+
debug('[Pulse Tracker Proxy] Sending message to target window', message);
|
|
62
|
+
target.postMessage(message, '*', [channel.port2]);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PulseTrackerProxyImpl = void 0;
|
|
7
|
+
const consts_1 = require("./consts");
|
|
8
|
+
const send_message_1 = require("./send-message");
|
|
9
|
+
const version_json_1 = __importDefault(require("./version.json"));
|
|
10
|
+
/**
|
|
11
|
+
* Adds proxy version information to the event input.
|
|
12
|
+
* This helps with debugging and allows tracking which proxy version was used for the event.
|
|
13
|
+
*
|
|
14
|
+
* @param input - The original event input to be decorated
|
|
15
|
+
* @returns The input with added proxyVersion in the tracker object
|
|
16
|
+
*/
|
|
17
|
+
function decorateWithProxyVersion(input) {
|
|
18
|
+
const tracker = 'tracker' in input && typeof input.tracker === 'object' ? input.tracker : {};
|
|
19
|
+
return {
|
|
20
|
+
...input,
|
|
21
|
+
tracker: { ...tracker, proxyVersion: version_json_1.default.PROXY_VERSION },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Implementation of the Pulse Tracker Proxy that forwards tracking calls to a target window.
|
|
26
|
+
* This class handles the communication between frames/windows and ensures proper message handling.
|
|
27
|
+
*/
|
|
28
|
+
class PulseTrackerProxyImpl {
|
|
29
|
+
constructor(target, debug) {
|
|
30
|
+
this.target = target;
|
|
31
|
+
this.debug = debug;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Tracks an event by forwarding the call to the target window.
|
|
35
|
+
* Automatically decorates the event with proxy version information.
|
|
36
|
+
*
|
|
37
|
+
* @param args - Arguments that would normally be passed to the Pulse tracker's track method
|
|
38
|
+
* @returns A promise that resolves with the response from the target window
|
|
39
|
+
*/
|
|
40
|
+
track(...args) {
|
|
41
|
+
this.debug('[Pulse Tracker Proxy] Calling `track`', args);
|
|
42
|
+
const [eventType, input, ...rest] = args;
|
|
43
|
+
const modifiedArgs = [eventType, decorateWithProxyVersion(input), ...rest];
|
|
44
|
+
return this.invoke({ method: 'track', args: modifiedArgs });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Invokes a remote function in the target window and handles the response.
|
|
48
|
+
*
|
|
49
|
+
* @param invocation - The invocation details including method name and arguments
|
|
50
|
+
* @returns A promise that resolves with the response payload or rejects with an error
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
async invoke(invocation) {
|
|
54
|
+
this.debug('[Pulse Tracker Proxy] Invoking remote function', invocation);
|
|
55
|
+
const response = await (0, send_message_1.sendMessage)({ type: consts_1.MESSAGE_TYPE_INVOKE, payload: invocation }, { target: this.target });
|
|
56
|
+
if (!(0, consts_1.isPulseProxyMessage)(response)) {
|
|
57
|
+
this.debug('[Pulse Tracker Proxy] Invalid response from proxy', response);
|
|
58
|
+
throw new Error('Invalid response from proxy');
|
|
59
|
+
}
|
|
60
|
+
if (response.type === consts_1.MESSAGE_TYPE_RETURN_ERROR) {
|
|
61
|
+
this.debug('[Pulse Tracker Proxy] Error response from proxy', response.error);
|
|
62
|
+
throw new Error(response.error);
|
|
63
|
+
}
|
|
64
|
+
if (response.type === consts_1.MESSAGE_TYPE_RETURN) {
|
|
65
|
+
this.debug('[Pulse Tracker Proxy] Successful response from proxy', response.payload);
|
|
66
|
+
return response.payload;
|
|
67
|
+
}
|
|
68
|
+
this.debug('[Pulse Tracker Proxy] Unhandled response from proxy', response);
|
|
69
|
+
throw new Error('Unexpected response from proxy');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.PulseTrackerProxyImpl = PulseTrackerProxyImpl;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "PROXY_VERSION": "1.0.0", "GIT_COMMIT": "6ca09707" }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Sync the content between files below this line:
|
|
2
|
+
export const MESSAGE_PREFIX = 'pulse-tracker-proxy';
|
|
3
|
+
export const MESSAGE_TYPE_REQUEST = `${MESSAGE_PREFIX}:request`;
|
|
4
|
+
export const MESSAGE_TYPE_ACK = `${MESSAGE_PREFIX}:ack`;
|
|
5
|
+
export const MESSAGE_TYPE_INVOKE = `${MESSAGE_PREFIX}:invoke`;
|
|
6
|
+
export const MESSAGE_TYPE_RETURN = `${MESSAGE_PREFIX}:return`;
|
|
7
|
+
export const MESSAGE_TYPE_RETURN_ERROR = `${MESSAGE_PREFIX}:return-error`;
|
|
8
|
+
export const ALLOWED_METHODS = ['track'];
|
|
9
|
+
export function isPulseProxyMessage(message) {
|
|
10
|
+
return (message !== null &&
|
|
11
|
+
typeof message === 'object' &&
|
|
12
|
+
'type' in message &&
|
|
13
|
+
typeof message.type === 'string' &&
|
|
14
|
+
message.type.startsWith(MESSAGE_PREFIX));
|
|
15
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { MESSAGE_TYPE_REQUEST } from './consts';
|
|
2
|
+
import { loadAutotracker } from './load-autotracker';
|
|
3
|
+
import { sendMessage } from './send-message';
|
|
4
|
+
import { PulseTrackerProxyImpl } from './tracker-proxy';
|
|
5
|
+
/**
|
|
6
|
+
* Pulse Tracker Proxy System Architecture
|
|
7
|
+
* --------------------------------------
|
|
8
|
+
* The proxy system allows for cross-frame/cross-window tracking events without directly exposing
|
|
9
|
+
* the tracking API to child frames. It works in two modes:
|
|
10
|
+
*
|
|
11
|
+
* 1. Mobile App Webview Mode:
|
|
12
|
+
* - Detects if running in a mobile app webview by checking for `pulseEventHandler`
|
|
13
|
+
* - Loads the Pulse autotracker with the specified version and provider
|
|
14
|
+
* - Directly communicates with the native app's tracking system
|
|
15
|
+
*
|
|
16
|
+
* 2. Iframe/Cross-window Mode:
|
|
17
|
+
* - Establishes a connection with the parent window (or specified target)
|
|
18
|
+
* - Uses postMessage for communication
|
|
19
|
+
* - Handles timeouts, retries, and error cases for reliable messaging
|
|
20
|
+
* - Proxies tracking calls to the parent window which can then handle them
|
|
21
|
+
*
|
|
22
|
+
* The system adds a proxy version to all events to help with debugging and tracking.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Establishes a connection with the target frame and returns a pulse tracker proxy object.
|
|
26
|
+
* This function automatically detects whether it's running in a mobile app webview or
|
|
27
|
+
* needs to use iframe/cross-window communication.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const tracker = await getPulseTrackerProxy({
|
|
32
|
+
* provider: 'my-provider',
|
|
33
|
+
* debug: console.log
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* tracker.track('trackerEvent', { ... });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @param options - Configuration options for the pulse tracker proxy
|
|
40
|
+
* @returns A promise that resolves to a proxy object implementing the Pulse tracker interface
|
|
41
|
+
* @throws Error if connection fails, timeout occurs, or required options are missing
|
|
42
|
+
*/
|
|
43
|
+
export function getPulseTrackerProxy({ provider, autotrackerVersion = '3', window = globalThis.window, target = window.parent, timeout = 5000, retries = 3, retryTimeout = 1000, debug = () => { }, } = {}) {
|
|
44
|
+
debug('[Pulse Tracker Proxy] Checking if `pulseEventHandler` is provided in the current window...');
|
|
45
|
+
if (window.pulseEventHandler) {
|
|
46
|
+
debug(`[Pulse Tracker Proxy] \`pulseEventHandler\` found in the current window. Assuming it is the app webview. Loading autotracker version ${autotrackerVersion}...`);
|
|
47
|
+
if (!provider) {
|
|
48
|
+
debug('[Pulse Tracker Proxy] `provider` is not set - cannot create pulse tracker.');
|
|
49
|
+
return Promise.reject('`provider` is not set - cannot create pulse tracker.');
|
|
50
|
+
}
|
|
51
|
+
return loadAutotracker(window, provider, autotrackerVersion);
|
|
52
|
+
}
|
|
53
|
+
debug('[Pulse Tracker Proxy] `pulseEventHandler` not found in the current window - not a mobile app webview.');
|
|
54
|
+
debug('[Pulse Tracker Proxy] Trying to establish connection...', { target, timeout, retries });
|
|
55
|
+
if (!target) {
|
|
56
|
+
return Promise.reject(new Error('Target window not provided. Possibly window.parent is not available.'));
|
|
57
|
+
}
|
|
58
|
+
let ackTimeout;
|
|
59
|
+
let alreadyTimedOut = false;
|
|
60
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
61
|
+
ackTimeout = setTimeout(() => {
|
|
62
|
+
alreadyTimedOut = true;
|
|
63
|
+
debug('[Pulse Tracker Proxy] Ack timeout. Sorry, target seems to not support the proxy.');
|
|
64
|
+
reject(new Error(`Pulse tracker proxy not found within ${timeout} ms`));
|
|
65
|
+
}, timeout);
|
|
66
|
+
});
|
|
67
|
+
const connectionPromise = (async () => {
|
|
68
|
+
for (let i = 0; i < retries; i++) {
|
|
69
|
+
if (alreadyTimedOut) {
|
|
70
|
+
debug('[Pulse Tracker Proxy] Configured timeout has passed before the next attempt');
|
|
71
|
+
throw new Error('Configured timeout has passed before the next attempt.');
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
debug(`[Pulse Tracker Proxy] Sending request to target window (attempt: ${i + 1})...`);
|
|
75
|
+
await sendMessage({ type: MESSAGE_TYPE_REQUEST }, { target, timeout: retryTimeout });
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
debug(`[Pulse Tracker Proxy] Ack Request failed: ${error}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
clearTimeout(ackTimeout);
|
|
82
|
+
debug(`[Pulse Tracker Proxy] Ack Request successful (attempt: ${i + 1})`);
|
|
83
|
+
return new PulseTrackerProxyImpl(target, debug);
|
|
84
|
+
}
|
|
85
|
+
clearTimeout(ackTimeout);
|
|
86
|
+
debug('[Pulse Tracker Proxy] Retries exhausted. Target did not respond. Failing.');
|
|
87
|
+
throw new Error('Failed to establish connection with pulse tracker proxy');
|
|
88
|
+
})();
|
|
89
|
+
return Promise.race([timeoutPromise, connectionPromise]);
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './get-proxy';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads the Pulse autotracker script onto the page and initializes it with the provided configuration.
|
|
3
|
+
*
|
|
4
|
+
* @param window - The browser window object to attach the script to
|
|
5
|
+
* @param provider - The provider ID to initialize the Pulse tracker with
|
|
6
|
+
* @param version - The version of the Pulse autotracker to load
|
|
7
|
+
* @returns A promise that resolves to the configured Tracker instance
|
|
8
|
+
* @throws Error if script loading fails or Pulse doesn't initialize correctly
|
|
9
|
+
*/
|
|
10
|
+
export async function loadAutotracker(window, provider, version) {
|
|
11
|
+
await new Promise((resolve, reject) => {
|
|
12
|
+
const element = window.document.createElement('script');
|
|
13
|
+
element.src = `//sdk.pulse.schibsted.com/versioned/${version}/pulse.min.js`;
|
|
14
|
+
element.async = true;
|
|
15
|
+
element.onload = resolve;
|
|
16
|
+
element.onerror = () => {
|
|
17
|
+
reject(new Error(`Failed to load autotracker script from src: ${element.src}`));
|
|
18
|
+
};
|
|
19
|
+
window.document.head.appendChild(element);
|
|
20
|
+
});
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
if (!window.pulse) {
|
|
23
|
+
reject(new Error('Pulse autotracker did not load correctly!'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
window.pulse('init', provider);
|
|
27
|
+
window.pulse((tracker) => {
|
|
28
|
+
resolve(tracker);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isPulseProxyMessage, MESSAGE_TYPE_RETURN_ERROR } from './consts';
|
|
2
|
+
/**
|
|
3
|
+
* Sends a message to a target window and returns a promise that resolves with the response.
|
|
4
|
+
*
|
|
5
|
+
* This function creates a MessageChannel to facilitate two-way communication with the target.
|
|
6
|
+
* It handles timeouts and error responses appropriately.
|
|
7
|
+
*
|
|
8
|
+
* @param message - The message object to send to the target window
|
|
9
|
+
* @param options - Configuration options for sending the message
|
|
10
|
+
* @param options.target - The target window to send the message to (e.g., iframe.contentWindow)
|
|
11
|
+
* @param options.timeout - Optional timeout in milliseconds after which the promise will reject
|
|
12
|
+
* @param options.debug - Optional debug function for logging message events
|
|
13
|
+
* @returns A promise that resolves with the response message or rejects with an error
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { sendMessage } from './send-message';
|
|
18
|
+
*
|
|
19
|
+
* const message = { type: 'SOME_MESSAGE_TYPE', payload: { ... } };
|
|
20
|
+
* sendMessage(message, {
|
|
21
|
+
* target: document.querySelector('iframe').contentWindow,
|
|
22
|
+
* timeout: 5000,
|
|
23
|
+
* debug: console.log
|
|
24
|
+
* }).then(response => {
|
|
25
|
+
* // Handle response
|
|
26
|
+
* }).catch(error => {
|
|
27
|
+
* // Handle error
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function sendMessage(message, { target, timeout, debug = () => { } }) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const channel = new MessageChannel();
|
|
34
|
+
const requestTimeout = timeout
|
|
35
|
+
? setTimeout(() => {
|
|
36
|
+
channel.port1.close();
|
|
37
|
+
reject(new Error(`Message of type ${message.type} timed out`));
|
|
38
|
+
}, timeout)
|
|
39
|
+
: undefined;
|
|
40
|
+
const responseHandler = (event) => {
|
|
41
|
+
debug('[Pulse Tracker Proxy] Received the response on the MessageChannel', event);
|
|
42
|
+
if (!isPulseProxyMessage(event.data)) {
|
|
43
|
+
debug('[Pulse Tracker Proxy] Response was not a valid Proxy Message', event.data);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (event.data.type === MESSAGE_TYPE_RETURN_ERROR) {
|
|
47
|
+
debug('[Pulse Tracker Proxy] Received an return error response', event.data.error);
|
|
48
|
+
clearTimeout(requestTimeout);
|
|
49
|
+
channel.port1.close();
|
|
50
|
+
reject(new Error(event.data.error));
|
|
51
|
+
}
|
|
52
|
+
debug('[Pulse Tracker Proxy] Received a valid response', event.data);
|
|
53
|
+
clearTimeout(requestTimeout);
|
|
54
|
+
channel.port1.close();
|
|
55
|
+
resolve(event.data);
|
|
56
|
+
};
|
|
57
|
+
channel.port1.onmessage = responseHandler;
|
|
58
|
+
debug('[Pulse Tracker Proxy] Sending message to target window', message);
|
|
59
|
+
target.postMessage(message, '*', [channel.port2]);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isPulseProxyMessage, MESSAGE_TYPE_INVOKE, MESSAGE_TYPE_RETURN, MESSAGE_TYPE_RETURN_ERROR, } from './consts';
|
|
2
|
+
import { sendMessage } from './send-message';
|
|
3
|
+
import version from './version.json';
|
|
4
|
+
/**
|
|
5
|
+
* Adds proxy version information to the event input.
|
|
6
|
+
* This helps with debugging and allows tracking which proxy version was used for the event.
|
|
7
|
+
*
|
|
8
|
+
* @param input - The original event input to be decorated
|
|
9
|
+
* @returns The input with added proxyVersion in the tracker object
|
|
10
|
+
*/
|
|
11
|
+
function decorateWithProxyVersion(input) {
|
|
12
|
+
const tracker = 'tracker' in input && typeof input.tracker === 'object' ? input.tracker : {};
|
|
13
|
+
return {
|
|
14
|
+
...input,
|
|
15
|
+
tracker: { ...tracker, proxyVersion: version.PROXY_VERSION },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Implementation of the Pulse Tracker Proxy that forwards tracking calls to a target window.
|
|
20
|
+
* This class handles the communication between frames/windows and ensures proper message handling.
|
|
21
|
+
*/
|
|
22
|
+
export class PulseTrackerProxyImpl {
|
|
23
|
+
constructor(target, debug) {
|
|
24
|
+
this.target = target;
|
|
25
|
+
this.debug = debug;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Tracks an event by forwarding the call to the target window.
|
|
29
|
+
* Automatically decorates the event with proxy version information.
|
|
30
|
+
*
|
|
31
|
+
* @param args - Arguments that would normally be passed to the Pulse tracker's track method
|
|
32
|
+
* @returns A promise that resolves with the response from the target window
|
|
33
|
+
*/
|
|
34
|
+
track(...args) {
|
|
35
|
+
this.debug('[Pulse Tracker Proxy] Calling `track`', args);
|
|
36
|
+
const [eventType, input, ...rest] = args;
|
|
37
|
+
const modifiedArgs = [eventType, decorateWithProxyVersion(input), ...rest];
|
|
38
|
+
return this.invoke({ method: 'track', args: modifiedArgs });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Invokes a remote function in the target window and handles the response.
|
|
42
|
+
*
|
|
43
|
+
* @param invocation - The invocation details including method name and arguments
|
|
44
|
+
* @returns A promise that resolves with the response payload or rejects with an error
|
|
45
|
+
* @private
|
|
46
|
+
*/
|
|
47
|
+
async invoke(invocation) {
|
|
48
|
+
this.debug('[Pulse Tracker Proxy] Invoking remote function', invocation);
|
|
49
|
+
const response = await sendMessage({ type: MESSAGE_TYPE_INVOKE, payload: invocation }, { target: this.target });
|
|
50
|
+
if (!isPulseProxyMessage(response)) {
|
|
51
|
+
this.debug('[Pulse Tracker Proxy] Invalid response from proxy', response);
|
|
52
|
+
throw new Error('Invalid response from proxy');
|
|
53
|
+
}
|
|
54
|
+
if (response.type === MESSAGE_TYPE_RETURN_ERROR) {
|
|
55
|
+
this.debug('[Pulse Tracker Proxy] Error response from proxy', response.error);
|
|
56
|
+
throw new Error(response.error);
|
|
57
|
+
}
|
|
58
|
+
if (response.type === MESSAGE_TYPE_RETURN) {
|
|
59
|
+
this.debug('[Pulse Tracker Proxy] Successful response from proxy', response.payload);
|
|
60
|
+
return response.payload;
|
|
61
|
+
}
|
|
62
|
+
this.debug('[Pulse Tracker Proxy] Unhandled response from proxy', response);
|
|
63
|
+
throw new Error('Unexpected response from proxy');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "PROXY_VERSION": "1.0.0", "GIT_COMMIT": "6ca09707" }
|