|
| 1 | +// Copyright (c) 2026 The Jaeger Authors. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +// Mock dependencies |
| 5 | +jest.mock('../constants', () => ({ |
| 6 | + getAppEnvironment: jest.fn(() => 'production'), |
| 7 | + shouldDebugGoogleAnalytics: jest.fn(() => true), |
| 8 | +})); |
| 9 | + |
| 10 | +jest.mock('../prefix-url', () => s => s); |
| 11 | + |
| 12 | +describe('GA Coverage', () => { |
| 13 | + let originalFetch; |
| 14 | + let mockFetch; |
| 15 | + let GA; |
| 16 | + let trackNavigation; |
| 17 | + let captureException; |
| 18 | + |
| 19 | + beforeEach(() => { |
| 20 | + jest.resetModules(); |
| 21 | + |
| 22 | + window.dataLayer = []; |
| 23 | + jest.clearAllMocks(); |
| 24 | + originalFetch = window.fetch; |
| 25 | + mockFetch = jest.fn().mockResolvedValue({ status: 200, json: () => ({}) }); |
| 26 | + window.fetch = mockFetch; |
| 27 | + |
| 28 | + // Require modules after reset |
| 29 | + GA = require('./ga').default; |
| 30 | + const ec = require('./error-capture'); |
| 31 | + trackNavigation = ec.trackNavigation; |
| 32 | + captureException = ec.captureException; |
| 33 | + |
| 34 | + // Mock site-prefix via doMock since it might be cached? |
| 35 | + // jest.mock persists across resetModules if defined outside? |
| 36 | + // It's safer to rely on the top level mock or define it here if needed. |
| 37 | + jest.doMock('../../site-prefix', () => '/prefix/'); |
| 38 | + }); |
| 39 | + |
| 40 | + afterEach(() => { |
| 41 | + window.fetch = originalFetch; |
| 42 | + }); |
| 43 | + |
| 44 | + const config = { |
| 45 | + tracking: { |
| 46 | + gaID: 'UA-TEST', |
| 47 | + trackErrors: true, |
| 48 | + cookiesToDimensions: [ |
| 49 | + { cookie: 'my-cookie', dimension: 'dim1' }, |
| 50 | + { cookie: 'missing-cookie', dimension: 'dim2' }, |
| 51 | + ], |
| 52 | + }, |
| 53 | + }; |
| 54 | + |
| 55 | + it('initializes with cookies to dimensions', () => { |
| 56 | + document.cookie = 'my-cookie=foo'; |
| 57 | + const ga = GA(config, 'v1', 'v1-long'); |
| 58 | + ga.init(); |
| 59 | + |
| 60 | + const setCmd = window.dataLayer.find(cmd => cmd[0] === 'set' && cmd[1].dim1 === 'foo'); |
| 61 | + expect(setCmd).toBeDefined(); |
| 62 | + }); |
| 63 | + |
| 64 | + it('formats various breadcrumbs and error info', async () => { |
| 65 | + const ga = GA(config, 'v1', 'v1-long'); |
| 66 | + ga.init(); |
| 67 | + |
| 68 | + // 1. Navigation crumbs |
| 69 | + trackNavigation('/dependencies'); // dp |
| 70 | + trackNavigation('/trace/123'); // tr |
| 71 | + trackNavigation('/search?x=y'); // sd |
| 72 | + trackNavigation('/search'); // sr |
| 73 | + trackNavigation('/'); // rt |
| 74 | + trackNavigation('/unknown'); // ?? |
| 75 | + window.history.pushState({}, '', '/unknown'); |
| 76 | + |
| 77 | + // 2. Fetch crumbs |
| 78 | + await window.fetch('/api/services'); // svc |
| 79 | + await window.fetch('/api/unknown/operations'); // op |
| 80 | + await window.fetch('/api/traces?service=x'); // sr |
| 81 | + await window.fetch('/api/traces/123'); // tr |
| 82 | + await window.fetch('/api/dependencies'); // dp |
| 83 | + await window.fetch('/foo.js'); // __IGNORE__ |
| 84 | + |
| 85 | + // Add non-200 fetch |
| 86 | + mockFetch.mockResolvedValueOnce({ status: 500 }); |
| 87 | + await window.fetch('/api/services'); // [svc|500] |
| 88 | + |
| 89 | + // 3. UI crumbs |
| 90 | + const div = document.createElement('div'); |
| 91 | + div.className = 'ub-ignore MyClass'; |
| 92 | + div.id = 'my-id'; |
| 93 | + document.body.appendChild(div); |
| 94 | + for (let i = 0; i < 5; i++) { |
| 95 | + div.click(); |
| 96 | + } |
| 97 | + |
| 98 | + // Fill breadcrumbs with errors to trigger truncation |
| 99 | + // 20 crumbs * 50 chars = 1000 chars > 498 |
| 100 | + for (let i = 0; i < 25; i++) { |
| 101 | + try { |
| 102 | + throw new Error('long error message filling up the breadcrumbs buffer ' + i); |
| 103 | + } catch (e) { |
| 104 | + captureException(e); |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + // 4. Trigger Final Error |
| 109 | + const error = new Error('Final Test Error'); |
| 110 | + captureException(error); |
| 111 | + |
| 112 | + // Verify GA event exception (last one) |
| 113 | + // window.dataLayer has many events now. Find the last one matching final error. |
| 114 | + const exceptionEvents = window.dataLayer.filter(e => e[0] === 'event' && e[1] === 'exception'); |
| 115 | + const lastException = exceptionEvents[exceptionEvents.length - 1]; |
| 116 | + expect(lastException).toBeDefined(); |
| 117 | + expect(lastException[2].description).toContain('! Final Test Error'); |
| 118 | + |
| 119 | + const eventCalls = window.dataLayer.filter( |
| 120 | + e => e[0] === 'event' && e[2].event_category === 'jaeger/??/error' |
| 121 | + ); |
| 122 | + const lastEvent = eventCalls[eventCalls.length - 1]; |
| 123 | + expect(lastEvent).toBeDefined(); |
| 124 | + |
| 125 | + const eventLabel = lastEvent[2].event_label; |
| 126 | + // Expect truncation. Start crumbs (nav, fetch) might be pushed out by buffer logic (20 limit). |
| 127 | + // But we filled with 25 errors. So only errors remain. |
| 128 | + // So checking for [svc] is invalid now as it was shifted out. |
| 129 | + |
| 130 | + // Check that label IS truncated (length check? or content check?) |
| 131 | + // Max length 499. |
| 132 | + expect(eventLabel.length).toBeLessThanOrEqual(499); |
| 133 | + // It should contain the last error messages |
| 134 | + expect(eventLabel).toContain('filling up the breadcrumbs buffer'); |
| 135 | + // It should NOT contain [svc] (shifted out) |
| 136 | + expect(eventLabel).not.toContain('[svc]'); |
| 137 | + }); |
| 138 | + |
| 139 | + it('handles stack trace missing', () => { |
| 140 | + const ga = GA(config, 'v1', 'v1-long'); |
| 141 | + ga.init(); |
| 142 | + |
| 143 | + class NoStackError extends Error { |
| 144 | + constructor(m) { |
| 145 | + super(m); |
| 146 | + this.stack = undefined; |
| 147 | + } |
| 148 | + } |
| 149 | + const error = new NoStackError('msg'); |
| 150 | + captureException(error); |
| 151 | + |
| 152 | + const exceptionEvent = window.dataLayer.find(e => e[0] === 'event' && e[1] === 'exception'); |
| 153 | + expect(exceptionEvent).toBeDefined(); |
| 154 | + }); |
| 155 | + |
| 156 | + it('truncates very long messages/labels', () => { |
| 157 | + const ga = GA(config, 'v1', 'v1-long'); |
| 158 | + ga.init(); |
| 159 | + |
| 160 | + const longMsg = 'a'.repeat(1000); |
| 161 | + const error = new Error(longMsg); |
| 162 | + captureException(error); |
| 163 | + |
| 164 | + const exceptionEvent = window.dataLayer.find(e => e[0] === 'event' && e[1] === 'exception'); |
| 165 | + expect(exceptionEvent[2].description.length).toBeLessThan(150); |
| 166 | + }); |
| 167 | +}); |
0 commit comments