• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kiva / ui / 30406725935

28 Jul 2026 11:03PM UTC coverage: 84.083% (-1.2%) from 85.238%
30406725935

push

github

web-flow
feat: meta event cleanup (#7108)

* fix: improvements to existing meta events

* fix: misc fixes and improvements to fb/meta tracking

* fix: rename shared fb event and add catch to plugin to match library

* fix: update analytics

* feat: misc cleanup and improvements to meta events

* fix: cleanup lock file

* fix: minor cleanup

6449 of 7219 branches covered (89.33%)

Branch coverage included in aggregate %.

136 of 143 new or added lines in 16 files covered. (95.1%)

1 existing line in 1 file now uncovered.

53864 of 64511 relevant lines covered (83.5%)

28.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.87
/src/plugins/kv-analytics-plugin.js
1
/* eslint-disable no-underscore-dangle */
1✔
2
import logFormatter from '#src/util/logFormatter';
1✔
3
import SimpleQueue from '#src/util/simpleQueue';
1✔
4
import {
1✔
5
        getUserTypeFromCookies,
6
        trackFBCustomEvent,
7
        trackFBPageView,
8
        trackFBTransaction,
9
} from '@kiva/kv-analytics';
10

11
// install method for plugin
1✔
12
export default {
1✔
13
        install: (app, { cookieStore } = {}) => {
1✔
14
                const inBrowser = typeof window !== 'undefined';
45✔
15
                let snowplowLoaded;
45✔
16
                let gtagLoaded;
45✔
17
                let optimizelyLoaded;
45✔
18
                const queue = new SimpleQueue();
45✔
19

20
                const kvActions = {
45✔
21
                        checkLibs: () => {
45✔
22
                                gtagLoaded = inBrowser && typeof window.gtag === 'function';
18✔
23
                                snowplowLoaded = inBrowser && typeof window.snowplow === 'function';
18✔
24
                                optimizelyLoaded = inBrowser && typeof window.optimizely?.push === 'function';
18✔
25

26
                                if (typeof window.gtag === 'function' && typeof window.snowplow === 'function') {
18✔
27
                                        return true;
17✔
28
                                }
17✔
29
                                return false;
1✔
30
                        },
45✔
31
                        pageview: (to, from) => {
45✔
32
                                if (!inBrowser) return false;
11!
33
                                kvActions.checkLibs();
11✔
34

35
                                let toUrl = typeof to === 'string' ? to : window.location.href;
11✔
36
                                let fromUrl = typeof from === 'string' ? from : document.referrer;
11✔
37

38
                                // update urls for async page changes
11✔
39
                                if (to && to.matched && to.matched.length) {
11✔
40
                                        toUrl = window.location.origin + to.fullPath;
3✔
41
                                }
3✔
42
                                if (from && from.matched && from.matched.length) {
11✔
43
                                        fromUrl = window.location.origin + from.fullPath;
1✔
44
                                }
1✔
45

46
                                // Snowplow pageview
11✔
47
                                if (snowplowLoaded) {
11✔
48
                                        // - snowplow seems to know better than the path rewriting performed by vue-router
10✔
49
                                        window.snowplow('setCustomUrl', toUrl);
10✔
50
                                        // set referrer for async page transitions
10✔
51
                                        if (from && from.matched && from.path !== '') {
10✔
52
                                                window.snowplow('setReferrerUrl', fromUrl); // asyncFromUrl
1✔
53
                                        }
1✔
54
                                        window.snowplow('trackPageView');
10✔
55
                                }
10✔
56

57
                                // Google Analytics gtag.js pageview
11✔
58
                                if (gtagLoaded) {
11✔
59
                                        let gaPath = `${window.location.pathname}${window.location.search || ''}`;
10✔
60
                                        if (to && to.matched && to.matched.length) {
10✔
61
                                                gaPath = to.fullPath;
3✔
62
                                        }
3✔
63
                                        window.gtag('event', 'page_view', {
10✔
64
                                                page_path: gaPath
10✔
65
                                        });
10✔
66
                                }
10✔
67

68
                                // Facebook pixel pageview
11✔
69
                                const userType = getUserTypeFromCookies(name => cookieStore?.get(name));
11✔
70
                                trackFBPageView(userType);
11✔
71
                        },
45✔
72
                        setCustomUrl: url => {
45✔
73
                                if (snowplowLoaded) {
2!
74
                                        window.snowplow('setCustomUrl', url);
×
75
                                }
×
76
                        },
45✔
77
                        trackEvent: (category, action, label, property, value, callback = () => {}) => {
45✔
78
                                const eventLabel = (label !== undefined && label !== null) ? String(label) : undefined;
7✔
79
                                const eventValue = (value !== undefined && value !== null) ? parseInt(value, 10) : undefined;
7✔
80
                                const eventProperty = (property !== undefined && property !== null) ? String(property) : undefined;
7✔
81

82
                                // Attempt gtag event
7✔
83
                                if (gtagLoaded) {
7!
84
                                        window.gtag('event', String(action), {
×
85
                                                event_category: String(category),
×
86
                                                event_label: eventLabel,
×
87
                                                value: eventValue
×
88
                                        });
×
89
                                }
×
90

91
                                // Attempt Snowplow event
7✔
92
                                if (snowplowLoaded) {
7!
93
                                        kvActions.trackSnowplowEvent({
×
94
                                                category,
×
95
                                                action,
×
96
                                                eventLabel,
×
97
                                                eventProperty,
×
98
                                                eventValue,
×
99
                                                callback
×
100
                                        });
×
101
                                } else {
7✔
102
                                        callback({ error: 'not loaded' });
7✔
103
                                        // add missed snowplow event to queue
7✔
104
                                        queue.add({
7✔
105
                                                eventType: 'trackSnowplowEvent',
7✔
106
                                                eventLib: 'snowplow',
7✔
107
                                                eventData: {
7✔
108
                                                        category,
7✔
109
                                                        action,
7✔
110
                                                        eventLabel,
7✔
111
                                                        eventProperty,
7✔
112
                                                        eventValue,
7✔
113
                                                        callback
7✔
114
                                                }
7✔
115
                                        });
7✔
116
                                }
7✔
117

118
                                return true;
7✔
119
                        },
45✔
120
                        trackSnowplowEvent: eventData => {
45✔
121
                                kvActions.checkLibs();
×
122
                                if (!snowplowLoaded) return false;
×
123

124
                                // In case there is a problem with the tracking event ensure that the callback gets called after 500ms
×
125
                                let callbackCalled = false;
×
126
                                const callbackTimeout = setTimeout(() => {
×
127
                                        if (!callbackCalled) {
×
128
                                                callbackCalled = true;
×
129
                                                eventData.callback({ error: 'timeout' });
×
130
                                        }
×
131
                                }, 500);
×
132

133
                                // Snowplow API
×
134
                                /* eslint-disable max-len */
×
135
                                // https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-tracker/tracking-specific-events/#tracking-custom-structured-events
×
136
                                // https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-tracker/tracking-specific-events/#callback-after-track-2-15-0
×
137
                                /* eslint-eable max-len */
×
138
                                // snowplow('trackStructEvent', 'category', 'action', 'label', 'property', 'value', context, timestamp, afterTrack);
×
139
                                window.snowplow(
×
140
                                        'trackStructEvent',
×
141
                                        eventData.category,
×
142
                                        eventData.action,
×
143
                                        eventData.eventLabel,
×
144
                                        eventData.eventProperty,
×
145
                                        eventData.eventValue,
×
146
                                        undefined,
×
147
                                        undefined,
×
148
                                        payload => {
×
149
                                                if (!callbackCalled) {
×
150
                                                        callbackCalled = true;
×
151
                                                        clearTimeout(callbackTimeout);
×
152
                                                        eventData.callback({ payload });
×
153
                                                }
×
154
                                        }
×
155
                                );
×
156
                        },
45✔
157
                        trackSelfDescribingEvent: eventData => {
45✔
158
                                // the data passed into this should be a JSON object similar to the following
2✔
159
                                // and should be defined by a schema in github.com/kiva/snowplow/tree/master/conf
2✔
160
                                // {
2✔
161
                                //     schema: 'https://raw.githubusercontent.com/kiva/...',
2✔
162
                                //     data: {
2✔
163
                                //         "loanId": 654321,
2✔
164
                                //         "amount": 500,
2✔
165
                                //                        ...
2✔
166
                                //     }
2✔
167
                                // });
2✔
168
                                if (snowplowLoaded) {
2!
169
                                        window.snowplow('trackSelfDescribingEvent', eventData);
×
170
                                } else {
2✔
171
                                        // add missed snowplow event to queue
2✔
172
                                        queue.add({
2✔
173
                                                eventType: 'trackSelfDescribingEvent',
2✔
174
                                                eventLib: 'snowplow',
2✔
175
                                                eventData,
2✔
176
                                        });
2✔
177
                                }
2✔
178

179
                                return true;
2✔
180
                        },
45✔
181
                        fireQueuedEvents() {
45✔
182
                                kvActions.checkLibs();
×
183

184
                                while (!queue.isEmpty()) {
×
185
                                        const item = queue.remove();
×
186
                                        const method = item.eventType;
×
187
                                        const { eventData } = item;
×
188
                                        if (inBrowser && typeof kvActions[method] === 'function') {
×
189
                                                // Wrapping the event call in a setTimeout ensures that this while loop
×
190
                                                // completes before the event functions are called. This is needed because
×
191
                                                // the event functions can add more events to this queue, and we only want
×
192
                                                // to process this queue once.
×
193
                                                window.setTimeout(() => {
×
194
                                                        kvActions[method](eventData, true);
×
195
                                                });
×
196
                                        }
×
197
                                }
×
198
                        },
45✔
199
                        parseEventProperties: eventValue => {
45✔
UNCOV
200
                                // Ensure we have a non-empty array to begin with
×
201
                                if (Array.isArray(eventValue) && eventValue.length) {
×
202
                                        // Handle multiple events being pass as an array
×
203
                                        if (Array.isArray(eventValue[0])) {
×
204
                                                eventValue.forEach(params => kvActions.trackEvent.apply(this, params));
×
205
                                        } else {
×
206
                                                kvActions.trackEvent.apply(this, eventValue);
×
207
                                        }
×
208
                                } else {
×
209
                                        throw new TypeError(`Expected non-empty array, but got ${eventValue}`);
×
210
                                }
×
211
                        },
45✔
212
                        trackTransaction: transactionData => {
45✔
213
                                kvActions.checkLibs();
7✔
214
                                if (!transactionData.transactionId) {
7✔
215
                                        return false;
2✔
216
                                }
2✔
217

218
                                trackFBTransaction(transactionData);
5✔
219
                                if (gtagLoaded) {
5✔
220
                                        kvActions.trackGATransaction(transactionData);
5✔
221
                                }
5✔
222
                                if (optimizelyLoaded) {
7✔
223
                                        kvActions.trackOPTransaction(transactionData);
4✔
224
                                }
4✔
225
                        },
45✔
226
                        trackGATransaction: transactionData => {
45✔
227
                                // push to dataLayer
5✔
228
                                if (typeof window.dataLayer === 'object') {
5✔
229
                                        window.dataLayer.push({
5✔
230
                                                event: 'setTransactionData',
5✔
231
                                                ...transactionData
5✔
232
                                        });
5✔
233
                                }
5✔
234

235
                                // Add each purchased item to the tracker
5✔
236
                                const allItems = transactionData.loans
5✔
237
                                        .concat(transactionData.donations)
5✔
238
                                        .concat(transactionData.kivaCards);
5✔
239

240
                                // Setup purchased items
5✔
241
                                const purchasedItems = allItems.map(item => {
5✔
242
                                        return {
6✔
243
                                                id: item.id,
6✔
244
                                                name: item.__typename,
6✔
245
                                                price: item.price,
6✔
246
                                                quantity: 1
6✔
247
                                        };
6✔
248
                                });
5✔
249

250
                                // Post transaction information to GA
5✔
251
                                window.gtag('event', 'purchase', {
5✔
252
                                        transaction_id: transactionData.transactionId,
5✔
253
                                        value: transactionData.itemTotal,
5✔
254
                                        currency: 'USD',
5✔
255
                                        items: purchasedItems,
5✔
256
                                        non_interaction: true
5✔
257
                                });
5✔
258
                        },
45✔
259
                        trackOPTransaction: transactionData => {
45✔
260
                                if (transactionData.depositTotal > 0) {
4✔
261
                                        window.optimizely.push({
1✔
262
                                                type: 'event',
1✔
263
                                                eventName: 'deposit',
1✔
264
                                                tags: {
1✔
265
                                                        revenue: transactionData.depositTotal * 100,
1✔
266
                                                        deposit_amount: transactionData.depositTotal
1✔
267
                                                }
1✔
268
                                        });
1✔
269
                                }
1✔
270

271
                                if (transactionData.loanTotal > 0) {
4✔
272
                                        window.optimizely.push({
3✔
273
                                                type: 'event',
3✔
274
                                                eventName: 'loan_share_purchase',
3✔
275
                                                tags: {
3✔
276
                                                        revenue: transactionData.loanTotal * 100,
3✔
277
                                                        loan_share_purchase_amount: transactionData.loanTotal
3✔
278
                                                }
3✔
279
                                        });
3✔
280
                                }
3✔
281

282
                                if (transactionData.donationTotal > 0) {
4✔
283
                                        window.optimizely.push({
1✔
284
                                                type: 'event',
1✔
285
                                                eventName: 'donation',
1✔
286
                                                tags: {
1✔
287
                                                        revenue: transactionData.donationTotal * 100,
1✔
288
                                                        donation_amount: transactionData.donationTotal
1✔
289
                                                }
1✔
290
                                        });
1✔
291
                                }
1✔
292
                        }
4✔
293
                };
45✔
294

295
                app.directive('kv-track-event', {
45✔
296
                        beforeMount: (el, binding) => {
45✔
297
                                // TODO: add arg for once, submit + change events
1✔
298
                                if (typeof el === 'object' && binding.value) {
1✔
299
                                        el.addEventListener('click', () => {
1✔
300
                                                try {
×
301
                                                        kvActions.parseEventProperties(binding.value);
×
302
                                                } catch (e) {
×
303
                                                        logFormatter(e, 'error');
×
304
                                                }
×
305
                                        });
1✔
306
                                }
1✔
307
                        }
1✔
308
                });
45✔
309

310
                // eslint-disable-next-line no-param-reassign
45✔
311
                app.config.globalProperties.$setKvAnalyticsData = (userId = null) => {
45✔
312
                        return new Promise(resolve => {
3✔
313
                                let readyStateTimeout;
3✔
314
                                const readyStateInterval = window.setInterval(() => {
3✔
315
                                        if (kvActions.checkLibs()) {
×
316
                                                clearInterval(readyStateInterval);
×
317
                                                clearTimeout(readyStateTimeout);
×
318
                                                // Setup Global Snowplow
×
319
                                                if (snowplowLoaded) {
×
320
                                                        window.snowplow('setUserId', userId);
×
321
                                                }
×
322
                                                // Setup Global GA Data
×
323
                                                if (userId && gtagLoaded && window.__KV_CONFIG__ && window.__KV_CONFIG__.gaId) {
×
324
                                                        window.gtag('config', window.__KV_CONFIG__.gaId, {
×
325
                                                                user_id: userId,
×
326
                                                                dimension1: userId,
×
327
                                                                send_page_view: false
×
328
                                                        });
×
329
                                                }
×
330
                                                // set id on dataLayer
×
331
                                                if (userId && typeof window.dataLayer === 'object') {
×
332
                                                        window.dataLayer.push({
×
333
                                                                kvuid: userId
×
334
                                                        });
×
335
                                                }
×
336
                                                // resovle for next steps
×
337
                                                resolve();
×
338
                                        }
×
339
                                }, 100);
3✔
340

341
                                readyStateTimeout = window.setTimeout(() => {
3✔
342
                                        // clean up interval and timeout
×
343
                                        clearInterval(readyStateInterval);
×
344
                                        clearTimeout(readyStateTimeout);
×
345
                                        // resolve the promise
×
346
                                        resolve();
×
347
                                }, 3000);
3✔
348
                        });
3✔
349
                };
45✔
350

351
                // eslint-disable-next-line no-param-reassign
45✔
352
                app.config.globalProperties.$fireAsyncPageView = (to, from) => {
45✔
353
                        kvActions.pageview(to, from);
10✔
354
                };
45✔
355

356
                // eslint-disable-next-line no-param-reassign
45✔
357
                app.config.globalProperties.$fireServerPageView = () => {
45✔
358
                        const to = { path: window.location.pathname };
1✔
359
                        const from = { path: document.referrer };
1✔
360
                        // delay pageview call to ensure window.performance.timing is fully populated
1✔
361
                        let pageviewFired = false;
1✔
362
                        // fallback if readyState = complete is delayed
1✔
363
                        const fallbackPageview = setTimeout(() => {
1✔
364
                                pageviewFired = true;
×
365
                                kvActions.pageview(to, from);
×
366
                        }, 500);
1✔
367
                        document.onreadystatechange = () => {
1✔
368
                                // fire on complete if not already fired
1✔
369
                                if (document.readyState === 'complete') {
1✔
370
                                        if (!pageviewFired) {
1✔
371
                                                clearTimeout(fallbackPageview);
1✔
372
                                                kvActions.pageview(to, from);
1✔
373
                                        }
1✔
374
                                }
1✔
375
                        };
1✔
376
                };
45✔
377

378
                // eslint-disable-next-line no-param-reassign
45✔
379
                app.config.globalProperties.$fireQueuedEvents = () => {
45✔
380
                        kvActions.fireQueuedEvents();
×
381
                };
45✔
382

383
                // eslint-disable-next-line no-param-reassign
45✔
384
                app.config.globalProperties.$kvSetCustomUrl = (url = window.location.href) => {
45✔
385
                        kvActions.setCustomUrl(url);
2✔
386
                };
45✔
387

388
                // eslint-disable-next-line no-param-reassign
45✔
389
                app.config.globalProperties.$kvTrackEvent = (category, action, label, property, value, callback) => {
45✔
390
                        kvActions.trackEvent(category, action, label, property, value, callback);
7✔
391
                };
45✔
392

393
                // eslint-disable-next-line no-param-reassign
45✔
394
                app.config.globalProperties.$kvTrackSelfDescribingEvent = data => {
45✔
395
                        kvActions.trackSelfDescribingEvent(data);
2✔
396
                };
45✔
397

398
                // eslint-disable-next-line no-param-reassign
45✔
399
                app.config.globalProperties.$kvTrackTransaction = transactionData => {
45✔
400
                        kvActions.trackTransaction(transactionData);
7✔
401
                };
45✔
402

403
                // eslint-disable-next-line no-param-reassign
45✔
404
                app.config.globalProperties.$kvTrackFBCustomEvent = (eventName, eventData = null) => {
45✔
405
                        trackFBCustomEvent(eventName, eventData);
2✔
406
                };
45✔
407
        }
45✔
408
};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc