1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | import { isStringArray, LOG } from '@snowplow/tracker-core';
|
32 | import {
|
33 | isFunction,
|
34 | addTracker,
|
35 | createSharedState,
|
36 | SharedState,
|
37 | BrowserTracker,
|
38 | addEventListener,
|
39 | getTrackers,
|
40 | } from '@snowplow/browser-tracker-core';
|
41 | import * as Snowplow from '@snowplow/browser-tracker';
|
42 | import { Plugins } from './features';
|
43 | import { JavaScriptTrackerConfiguration } from './configuration';
|
44 |
|
45 | declare global {
|
46 | interface Window {
|
47 | [key: string]: unknown;
|
48 | }
|
49 | }
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 | export interface Queue {
|
56 | |
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | push: (...args: any[]) => void;
|
65 | }
|
66 |
|
67 | interface PluginQueueItem {
|
68 | timeout: number;
|
69 | }
|
70 |
|
71 | type FunctionParameters = [Record<string, unknown>, Array<string>] | [Array<string>];
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | export function InQueueManager(functionName: string, asyncQueue: Array<unknown>): Queue {
|
80 | const windowAlias = window,
|
81 | documentAlias = document,
|
82 | sharedState: SharedState = createSharedState(),
|
83 | availableTrackerIds: Array<string> = [],
|
84 | pendingPlugins: Record<string, PluginQueueItem> = {},
|
85 | pendingQueue: Array<[string, FunctionParameters]> = [];
|
86 |
|
87 | let version: string, availableFunctions: Record<string, Function>;
|
88 | ({ version, ...availableFunctions } = Snowplow);
|
89 |
|
90 | function parseInputString(inputString: string): [string, string[] | undefined] {
|
91 | const separatedString = inputString.split(':'),
|
92 | extractedFunction = separatedString[0],
|
93 | extractedNames = separatedString.length > 1 ? separatedString[1].split(';') : undefined;
|
94 |
|
95 | return [extractedFunction, extractedNames];
|
96 | }
|
97 |
|
98 | function dispatch(f: string, parameters: FunctionParameters) {
|
99 | if (availableFunctions[f]) {
|
100 | try {
|
101 | availableFunctions[f].apply(null, parameters);
|
102 | } catch (ex) {
|
103 | LOG.error(f + ' failed', ex);
|
104 | }
|
105 | } else {
|
106 | LOG.warn(f + ' is not an available function');
|
107 | }
|
108 | }
|
109 |
|
110 | function tryProcessQueue() {
|
111 | if (Object.keys(pendingPlugins).length === 0) {
|
112 | pendingQueue.forEach((q) => {
|
113 | let fnParameters = q[1];
|
114 | if (
|
115 | typeof availableFunctions[q[0]] !== 'undefined' &&
|
116 | availableFunctions[q[0]].length > fnParameters.length &&
|
117 | Array.isArray(fnParameters[0])
|
118 | ) {
|
119 | fnParameters = [{}, fnParameters[0]];
|
120 | }
|
121 | dispatch(q[0], fnParameters);
|
122 | });
|
123 | }
|
124 | }
|
125 |
|
126 | function updateAvailableFunctions(newFunctions: Record<string, Function>) {
|
127 |
|
128 | availableFunctions = {
|
129 | ...availableFunctions,
|
130 | ...newFunctions,
|
131 | };
|
132 | }
|
133 |
|
134 | function newTracker(parameterArray: Array<unknown>) {
|
135 | if (
|
136 | typeof parameterArray[0] === 'string' &&
|
137 | typeof parameterArray[1] === 'string' &&
|
138 | (typeof parameterArray[2] === 'undefined' || typeof parameterArray[2] === 'object')
|
139 | ) {
|
140 | const trackerId = `${functionName}_${parameterArray[0]}`,
|
141 | trackerConfiguration = parameterArray[2] as JavaScriptTrackerConfiguration,
|
142 | plugins = Plugins(trackerConfiguration),
|
143 | tracker = addTracker(trackerId, parameterArray[0], `js-${version}`, parameterArray[1], sharedState, {
|
144 | ...trackerConfiguration,
|
145 | plugins: plugins.map((p) => p[0]),
|
146 | });
|
147 |
|
148 | if (tracker) {
|
149 | availableTrackerIds.push(tracker.id);
|
150 | } else {
|
151 | LOG.warn(parameterArray[0] + ' already exists');
|
152 | return;
|
153 | }
|
154 |
|
155 | plugins.forEach((p) => {
|
156 | updateAvailableFunctions(p[1]);
|
157 | });
|
158 | } else {
|
159 | LOG.error('newTracker failed', new Error('Invalid parameters'));
|
160 | }
|
161 | }
|
162 |
|
163 | function addPlugin(parameterArray: Array<unknown>, trackerIdentifiers: Array<string>) {
|
164 | function postScriptHandler(scriptSrc: string) {
|
165 | if (Object.prototype.hasOwnProperty.call(pendingPlugins, scriptSrc)) {
|
166 | windowAlias.clearTimeout(pendingPlugins[scriptSrc].timeout);
|
167 | delete pendingPlugins[scriptSrc];
|
168 | tryProcessQueue();
|
169 | }
|
170 | }
|
171 |
|
172 | if (
|
173 | typeof parameterArray[0] === 'string' &&
|
174 | isStringArray(parameterArray[1]) &&
|
175 | (typeof parameterArray[2] === 'undefined' || Array.isArray(parameterArray[2]))
|
176 | ) {
|
177 | const scriptSrc = parameterArray[0],
|
178 | constructorPath = parameterArray[1],
|
179 | constructorParams = parameterArray[2],
|
180 | pauseTracking = parameterArray[3] ?? true;
|
181 |
|
182 | if (pauseTracking) {
|
183 | const timeout = windowAlias.setTimeout(() => {
|
184 | postScriptHandler(scriptSrc);
|
185 | }, 5000);
|
186 | pendingPlugins[scriptSrc] = {
|
187 | timeout: timeout,
|
188 | };
|
189 | }
|
190 | const pluginScript = documentAlias.createElement('script');
|
191 | pluginScript.setAttribute('src', scriptSrc);
|
192 | pluginScript.setAttribute('async', '1');
|
193 | addEventListener(
|
194 | pluginScript,
|
195 | 'error',
|
196 | () => {
|
197 | postScriptHandler(scriptSrc);
|
198 | LOG.warn(`Failed to load plugin ${constructorPath[0]} from ${scriptSrc}`);
|
199 | },
|
200 | true
|
201 | );
|
202 | addEventListener(
|
203 | pluginScript,
|
204 | 'load',
|
205 | () => {
|
206 | const [windowFn, innerFn] = constructorPath,
|
207 | plugin = windowAlias[windowFn];
|
208 | if (plugin && typeof plugin === 'object') {
|
209 | const { [innerFn]: pluginConstructor, ...api } = plugin as Record<string, Function>;
|
210 | availableFunctions['addPlugin'].apply(null, [
|
211 | { plugin: pluginConstructor.apply(null, constructorParams) },
|
212 | trackerIdentifiers,
|
213 | ]);
|
214 | updateAvailableFunctions(api);
|
215 | }
|
216 | postScriptHandler(scriptSrc);
|
217 | },
|
218 | true
|
219 | );
|
220 | documentAlias.head.appendChild(pluginScript);
|
221 | return;
|
222 | }
|
223 |
|
224 | if (
|
225 | typeof parameterArray[0] === 'object' &&
|
226 | typeof parameterArray[1] === 'string' &&
|
227 | (typeof parameterArray[2] === 'undefined' || Array.isArray(parameterArray[2]))
|
228 | ) {
|
229 | const plugin = parameterArray[0],
|
230 | constructorPath = parameterArray[1],
|
231 | constructorParams = parameterArray[2];
|
232 | if (plugin) {
|
233 | const { [constructorPath]: pluginConstructor, ...api } = plugin as Record<string, Function>;
|
234 | availableFunctions['addPlugin'].apply(null, [
|
235 | { plugin: pluginConstructor.apply(null, constructorParams) },
|
236 | trackerIdentifiers,
|
237 | ]);
|
238 | updateAvailableFunctions(api);
|
239 | return;
|
240 | }
|
241 | }
|
242 |
|
243 | LOG.warn(`Failed to add Plugin: ${parameterArray[1]}`);
|
244 | }
|
245 |
|
246 | |
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 | function applyAsyncFunction(...args: any[]) {
|
255 |
|
256 | for (let i = 0; i < args.length; i += 1) {
|
257 | let parameterArray = args[i],
|
258 | input = Array.prototype.shift.call(parameterArray);
|
259 |
|
260 |
|
261 | if (isFunction(input)) {
|
262 | try {
|
263 | let fnTrackers: Record<string, BrowserTracker> = {};
|
264 | for (const tracker of getTrackers(availableTrackerIds)) {
|
265 |
|
266 | fnTrackers[tracker.id.replace(`${functionName}_`, '')] = tracker;
|
267 | }
|
268 | input.apply(fnTrackers, parameterArray);
|
269 | } catch (ex) {
|
270 | LOG.error('Tracker callback failed', ex);
|
271 | } finally {
|
272 | continue;
|
273 | }
|
274 | }
|
275 |
|
276 | let parsedString = parseInputString(input),
|
277 | f = parsedString[0],
|
278 | names = parsedString[1];
|
279 |
|
280 | if (f === 'newTracker') {
|
281 | newTracker(parameterArray);
|
282 | continue;
|
283 | }
|
284 |
|
285 | const trackerIdentifiers = names ? names.map((n) => `${functionName}_${n}`) : availableTrackerIds;
|
286 |
|
287 | if (f === 'addPlugin') {
|
288 | addPlugin(parameterArray, trackerIdentifiers);
|
289 | continue;
|
290 | }
|
291 |
|
292 | let fnParameters: FunctionParameters;
|
293 | if (typeof parameterArray[0] !== 'undefined') {
|
294 | fnParameters = [parameterArray[0], trackerIdentifiers];
|
295 | } else if (typeof availableFunctions[f] !== 'undefined') {
|
296 | fnParameters = availableFunctions[f].length === 2 ? [{}, trackerIdentifiers] : [trackerIdentifiers];
|
297 | } else {
|
298 | fnParameters = [trackerIdentifiers];
|
299 | }
|
300 |
|
301 | if (Object.keys(pendingPlugins).length > 0) {
|
302 | pendingQueue.push([f, fnParameters]);
|
303 | continue;
|
304 | }
|
305 |
|
306 | dispatch(f, fnParameters);
|
307 | }
|
308 | }
|
309 |
|
310 |
|
311 | for (let i = 0; i < asyncQueue.length; i++) {
|
312 | applyAsyncFunction(asyncQueue[i]);
|
313 | }
|
314 |
|
315 | return {
|
316 | push: applyAsyncFunction,
|
317 | };
|
318 | }
|