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

kiva / ui / 19147367510

06 Nov 2025 07:26PM UTC coverage: 91.443% (+41.5%) from 49.902%
19147367510

push

github

emuvente
test: refactor category-row-arrows-visible-mixin test with runner method

3722 of 3979 branches covered (93.54%)

Branch coverage included in aggregate %.

18923 of 20785 relevant lines covered (91.04%)

78.6 hits per line

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

75.21
/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

5
// install method for plugin
1✔
6
export default {
1✔
7
        install: app => {
1✔
8
                const inBrowser = typeof window !== 'undefined';
40✔
9
                let snowplowLoaded;
40✔
10
                let gtagLoaded;
40✔
11
                let fbLoaded;
40✔
12
                let optimizelyLoaded;
40✔
13
                const queue = new SimpleQueue();
40✔
14

15
                const kvActions = {
40✔
16
                        checkLibs: () => {
40✔
17
                                gtagLoaded = inBrowser && typeof window.gtag === 'function';
13✔
18
                                snowplowLoaded = inBrowser && typeof window.snowplow === 'function';
13✔
19
                                fbLoaded = inBrowser && typeof window.fbq === 'function';
13✔
20
                                optimizelyLoaded = inBrowser && typeof window.optimizely === 'object';
13✔
21

22
                                if (typeof window.gtag === 'function' && typeof window.snowplow === 'function') {
13✔
23
                                        return true;
12✔
24
                                }
12✔
25
                                return false;
1✔
26
                        },
40✔
27
                        pageview: (to, from) => {
40✔
28
                                if (!inBrowser) return false;
7!
29
                                kvActions.checkLibs();
7✔
30

31
                                let toUrl = typeof to === 'string' ? to : window.location.href;
7✔
32
                                let fromUrl = typeof from === 'string' ? from : document.referrer;
7✔
33

34
                                // update urls for async page changes
7✔
35
                                if (to && to.matched && to.matched.length) {
7✔
36
                                        toUrl = window.location.origin + to.fullPath;
3✔
37
                                }
3✔
38
                                if (from && from.matched && from.matched.length) {
7✔
39
                                        fromUrl = window.location.origin + from.fullPath;
1✔
40
                                }
1✔
41

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

53
                                // Google Analytics gtag.js pageview
7✔
54
                                if (gtagLoaded) {
7✔
55
                                        let gaPath = `${window.location.pathname}${window.location.search || ''}`;
6✔
56
                                        if (to && to.matched && to.matched.length) {
6✔
57
                                                gaPath = to.fullPath;
3✔
58
                                        }
3✔
59
                                        window.gtag('event', 'page_view', {
6✔
60
                                                page_path: gaPath
6✔
61
                                        });
6✔
62
                                }
6✔
63

64
                                // facebook pixel pageview
7✔
65
                                if (fbLoaded) {
7✔
66
                                        // we used to pass a user_type but it's always empty across the site
6✔
67
                                        // { user_type: '???'}
6✔
68
                                        window.fbq('track', 'PageView');
6✔
69
                                }
6✔
70
                        },
40✔
71
                        setCustomUrl: url => {
40✔
72
                                if (snowplowLoaded) {
2!
73
                                        window.snowplow('setCustomUrl', url);
×
74
                                }
×
75
                        },
40✔
76
                        trackEvent: (category, action, label, property, value, callback = () => {}) => {
40✔
77
                                const eventLabel = (label !== undefined && label !== null) ? String(label) : undefined;
7✔
78
                                const eventValue = (value !== undefined && value !== null) ? parseInt(value, 10) : undefined;
7✔
79
                                const eventProperty = (property !== undefined && property !== null) ? String(property) : undefined;
7✔
80

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

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

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

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

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

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

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

224
                                if (fbLoaded) {
5✔
225
                                        kvActions.trackFBTransaction(transactionData);
5✔
226
                                }
5✔
227
                                if (gtagLoaded) {
5✔
228
                                        kvActions.trackGATransaction(transactionData);
5✔
229
                                }
5✔
230
                                if (optimizelyLoaded) {
5✔
231
                                        kvActions.trackOPTransaction(transactionData);
5✔
232
                                }
5✔
233
                        },
40✔
234
                        trackFBTransaction: transactionData => {
40✔
235
                                const itemTotal = transactionData.itemTotal || '';
5!
236
                                if (typeof window.fbq !== 'undefined' && typeof itemTotal !== 'undefined') {
5✔
237
                                        window.fbq('track', 'Purchase', {
5✔
238
                                                currency: 'USD',
5✔
239
                                                value: itemTotal,
5✔
240
                                                content_type: transactionData.isFTD ? 'FirstTimeDepositor' : 'ReturningLender'
5✔
241
                                        });
5✔
242
                                }
5✔
243

244
                                // signify transaction has kiva cards
5✔
245
                                if (transactionData.kivaCards && transactionData.kivaCards.length) {
5✔
246
                                        kvActions.trackFBCustomEvent(
1✔
247
                                                'transactionContainsKivaCards',
1✔
248
                                                {
1✔
249
                                                        kivaCardTotal: transactionData.kivaCardTotal
1✔
250
                                                }
1✔
251
                                        );
1✔
252
                                }
1✔
253
                                // signifiy transaction ftd status
5✔
254
                                if (transactionData.isFTD && typeof itemTotal !== 'undefined') {
5✔
255
                                        kvActions.trackFBCustomEvent(
1✔
256
                                                'firstTimeDepositorTransaction',
1✔
257
                                                {
1✔
258
                                                        itemTotal
1✔
259
                                                }
1✔
260
                                        );
1✔
261
                                }
1✔
262
                        },
40✔
263
                        trackGATransaction: transactionData => {
40✔
264
                                // push to dataLayer
5✔
265
                                if (typeof window.dataLayer === 'object') {
5✔
266
                                        window.dataLayer.push({
5✔
267
                                                event: 'setTransactionData',
5✔
268
                                                ...transactionData
5✔
269
                                        });
5✔
270
                                }
5✔
271

272
                                // Add each purchased item to the tracker
5✔
273
                                const allItems = transactionData.loans.concat(transactionData.donations);
5✔
274

275
                                // Setup purchased items
5✔
276
                                const purchasedItems = allItems.map(item => {
5✔
277
                                        return {
5✔
278
                                                id: item.id,
5✔
279
                                                name: item.__typename,
5✔
280
                                                price: item.price,
5✔
281
                                                quantity: 1
5✔
282
                                        };
5✔
283
                                });
5✔
284

285
                                // Post transaction information to GA
5✔
286
                                window.gtag('event', 'purchase', {
5✔
287
                                        transaction_id: transactionData.transactionId,
5✔
288
                                        value: transactionData.itemTotal,
5✔
289
                                        currency: 'USD',
5✔
290
                                        items: purchasedItems,
5✔
291
                                        non_interaction: true
5✔
292
                                });
5✔
293
                        },
40✔
294
                        trackOPTransaction: transactionData => {
40✔
295
                                if (transactionData.depositTotal > 0) {
5✔
296
                                        window.optimizely.push({
1✔
297
                                                type: 'event',
1✔
298
                                                eventName: 'deposit',
1✔
299
                                                tags: {
1✔
300
                                                        revenue: transactionData.depositTotal * 100,
1✔
301
                                                        deposit_amount: transactionData.depositTotal
1✔
302
                                                }
1✔
303
                                        });
1✔
304
                                }
1✔
305

306
                                if (transactionData.loanTotal > 0) {
5✔
307
                                        window.optimizely.push({
3✔
308
                                                type: 'event',
3✔
309
                                                eventName: 'loan_share_purchase',
3✔
310
                                                tags: {
3✔
311
                                                        revenue: transactionData.loanTotal * 100,
3✔
312
                                                        loan_share_purchase_amount: transactionData.loanTotal
3✔
313
                                                }
3✔
314
                                        });
3✔
315
                                }
3✔
316

317
                                if (transactionData.donationTotal > 0) {
5✔
318
                                        window.optimizely.push({
1✔
319
                                                type: 'event',
1✔
320
                                                eventName: 'donation',
1✔
321
                                                tags: {
1✔
322
                                                        revenue: transactionData.donationTotal * 100,
1✔
323
                                                        donation_amount: transactionData.donationTotal
1✔
324
                                                }
1✔
325
                                        });
1✔
326
                                }
1✔
327
                        }
5✔
328
                };
40✔
329

330
                app.directive('kv-track-event', {
40✔
331
                        beforeMount: (el, binding) => {
40✔
332
                                // TODO: add arg for once, submit + change events
1✔
333
                                if (typeof el === 'object' && binding.value) {
1✔
334
                                        el.addEventListener('click', () => {
1✔
335
                                                try {
×
336
                                                        kvActions.parseEventProperties(binding.value);
×
337
                                                } catch (e) {
×
338
                                                        logFormatter(e, 'error');
×
339
                                                }
×
340
                                        });
1✔
341
                                }
1✔
342
                        }
1✔
343
                });
40✔
344

345
                // eslint-disable-next-line no-param-reassign
40✔
346
                app.config.globalProperties.$setKvAnalyticsData = (userId = null) => {
40✔
347
                        return new Promise(resolve => {
3✔
348
                                let readyStateTimeout;
3✔
349
                                const readyStateInterval = window.setInterval(() => {
3✔
350
                                        if (kvActions.checkLibs()) {
×
351
                                                clearInterval(readyStateInterval);
×
352
                                                clearTimeout(readyStateTimeout);
×
353
                                                // Setup Global Snowplow
×
354
                                                if (snowplowLoaded) {
×
355
                                                        window.snowplow('setUserId', userId);
×
356
                                                }
×
357
                                                // Setup Global GA Data
×
358
                                                if (userId && gtagLoaded && window.__KV_CONFIG__ && window.__KV_CONFIG__.gaId) {
×
359
                                                        window.gtag('config', window.__KV_CONFIG__.gaId, {
×
360
                                                                user_id: userId,
×
361
                                                                dimension1: userId,
×
362
                                                                send_page_view: false
×
363
                                                        });
×
364
                                                }
×
365
                                                // set id on dataLayer
×
366
                                                if (userId && typeof window.dataLayer === 'object') {
×
367
                                                        window.dataLayer.push({
×
368
                                                                kvuid: userId
×
369
                                                        });
×
370
                                                }
×
371
                                                // resovle for next steps
×
372
                                                resolve();
×
373
                                        }
×
374
                                }, 100);
3✔
375

376
                                readyStateTimeout = window.setTimeout(() => {
3✔
377
                                        // clean up interval and timeout
×
378
                                        clearInterval(readyStateInterval);
×
379
                                        clearTimeout(readyStateTimeout);
×
380
                                        // resolve the promise
×
381
                                        resolve();
×
382
                                }, 3000);
3✔
383
                        });
3✔
384
                };
40✔
385

386
                // eslint-disable-next-line no-param-reassign
40✔
387
                app.config.globalProperties.$fireAsyncPageView = (to, from) => {
40✔
388
                        kvActions.pageview(to, from);
6✔
389
                };
40✔
390

391
                // eslint-disable-next-line no-param-reassign
40✔
392
                app.config.globalProperties.$fireServerPageView = () => {
40✔
393
                        const to = { path: window.location.pathname };
1✔
394
                        const from = { path: document.referrer };
1✔
395
                        // delay pageview call to ensure window.performance.timing is fully populated
1✔
396
                        let pageviewFired = false;
1✔
397
                        // fallback if readyState = complete is delayed
1✔
398
                        const fallbackPageview = setTimeout(() => {
1✔
399
                                pageviewFired = true;
×
400
                                kvActions.pageview(to, from);
×
401
                        }, 500);
1✔
402
                        document.onreadystatechange = () => {
1✔
403
                                // fire on complete if not already fired
1✔
404
                                if (document.readyState === 'complete') {
1✔
405
                                        if (!pageviewFired) {
1✔
406
                                                clearTimeout(fallbackPageview);
1✔
407
                                                kvActions.pageview(to, from);
1✔
408
                                        }
1✔
409
                                }
1✔
410
                        };
1✔
411
                };
40✔
412

413
                // eslint-disable-next-line no-param-reassign
40✔
414
                app.config.globalProperties.$fireQueuedEvents = () => {
40✔
415
                        kvActions.fireQueuedEvents();
×
416
                };
40✔
417

418
                // eslint-disable-next-line no-param-reassign
40✔
419
                app.config.globalProperties.$kvSetCustomUrl = (url = window.location.href) => {
40✔
420
                        kvActions.setCustomUrl(url);
2✔
421
                };
40✔
422

423
                // eslint-disable-next-line no-param-reassign
40✔
424
                app.config.globalProperties.$kvTrackEvent = (category, action, label, property, value, callback) => {
40✔
425
                        kvActions.trackEvent(category, action, label, property, value, callback);
7✔
426
                };
40✔
427

428
                // eslint-disable-next-line no-param-reassign
40✔
429
                app.config.globalProperties.$kvTrackSelfDescribingEvent = data => {
40✔
430
                        kvActions.trackSelfDescribingEvent(data);
2✔
431
                };
40✔
432

433
                // eslint-disable-next-line no-param-reassign
40✔
434
                app.config.globalProperties.$kvTrackTransaction = transactionData => {
40✔
435
                        kvActions.trackTransaction(transactionData);
6✔
436
                };
40✔
437

438
                // eslint-disable-next-line no-param-reassign
40✔
439
                app.config.globalProperties.$kvTrackFBCustomEvent = (eventName, eventData = null) => {
40✔
440
                        kvActions.trackFBCustomEvent(eventName, eventData);
2✔
441
                };
40✔
442
        }
40✔
443
};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc