diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b39a2b4..70254fc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,10 +8,10 @@ jobs: CI_JOB_NUMBER: 1 steps: - uses: actions/checkout@v1 - - name: Use Node.js 20.x - uses: actions/setup-node@v1 + - name: Use Node.js from .nvmrc + uses: actions/setup-node@v3 with: - node-version: 20.x + node-version-file: '.nvmrc' - run: yarn install - run: yarn lint-test @@ -21,9 +21,9 @@ jobs: CI_JOB_NUMBER: 2 steps: - uses: actions/checkout@v1 - - name: Use Node.js 20.x - uses: actions/setup-node@v1 + - name: Use Node.js from .nvmrc + uses: actions/setup-node@v3 with: - node-version: 20.x + node-version-file: '.nvmrc' - run: yarn install - run: yarn build diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index b28f879..73df7c8 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: - node-version: 20 + node-version-file: '.nvmrc' registry-url: https://registry.npmjs.org/ - run: yarn - run: yarn lint-test diff --git a/.nvmrc b/.nvmrc index 67e145b..248216a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.18.0 +24.12.0 diff --git a/packages/javascript/README.md b/packages/javascript/README.md index de92d27..3b25134 100644 --- a/packages/javascript/README.md +++ b/packages/javascript/README.md @@ -88,6 +88,7 @@ Initialization settings: | `disableGlobalErrorsHandling` | boolean | optional | Do not initialize global errors handling | | `disableVueErrorHandler` | boolean | optional | Do not initialize Vue errors handling | | `consoleTracking` | boolean | optional | Initialize console logs tracking | +| `breadcrumbs` | false or BreadcrumbsOptions object | optional | Configure breadcrumbs tracking (see below) | | `beforeSend` | function(event) => event | optional | This Method allows you to filter any data you don't want sending to Hawk | Other available [initial settings](types/hawk-initial-settings.d.ts) are described at the type definition. @@ -145,6 +146,94 @@ hawk.setContext({ }); ``` +## Breadcrumbs + +Breadcrumbs track user interactions and events leading up to an error, providing context for debugging. + +### Default Configuration + +By default, breadcrumbs are enabled with tracking for fetch/XHR requests, navigation, and UI clicks: + +```js +const hawk = new HawkCatcher({ + token: 'INTEGRATION_TOKEN' + // breadcrumbs enabled by default +}); +``` + +### Disabling Breadcrumbs + +To disable breadcrumbs entirely: + +```js +const hawk = new HawkCatcher({ + token: 'INTEGRATION_TOKEN', + breadcrumbs: false +}); +``` + +### Custom Configuration + +Configure breadcrumbs tracking behavior: + +```js +const hawk = new HawkCatcher({ + token: 'INTEGRATION_TOKEN', + breadcrumbs: { + maxBreadcrumbs: 20, // Maximum breadcrumbs to store (default: 15) + maxValueLength: 512, // Max string length (default: 1024) + trackFetch: true, // Track fetch/XHR requests (default: true) + trackNavigation: true, // Track navigation events (default: true) + trackClicks: true, // Track UI clicks (default: true) + beforeBreadcrumb: (breadcrumb, hint) => { + // Filter or modify breadcrumbs before storing + if (breadcrumb.category === 'fetch' && breadcrumb.data?.url?.includes('/sensitive')) { + return null; // Discard this breadcrumb + } + return breadcrumb; + } + } +}); +``` + +### Breadcrumbs Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `maxBreadcrumbs` | `number` | `15` | Maximum number of breadcrumbs to store. When the limit is reached, oldest breadcrumbs are removed (FIFO). | +| `maxValueLength` | `number` | `1024` | Maximum length for string values in breadcrumb data. Longer strings will be trimmed with `…` suffix. | +| `trackFetch` | `boolean` | `true` | Automatically track `fetch()` and `XMLHttpRequest` calls as breadcrumbs. Captures request URL, method, status code, and response time. | +| `trackNavigation` | `boolean` | `true` | Automatically track navigation events (History API: `pushState`, `replaceState`, `popstate`). Captures route changes. | +| `trackClicks` | `boolean` | `true` | Automatically track UI click events. Captures element selector, coordinates, and other click metadata. | +| `beforeBreadcrumb` | `function` | `undefined` | Hook called before each breadcrumb is stored. Receives `(breadcrumb, hint)` and can return modified breadcrumb, `null` to discard it, or the original breadcrumb. Useful for filtering sensitive data or PII. | + +### Manual Breadcrumbs + +Add custom breadcrumbs manually: + +```js +hawk.breadcrumbs.add({ + type: 'logic', + category: 'auth', + message: 'User logged in', + level: 'info', + data: { userId: '123' } +}); +``` + +### Breadcrumb Methods + +```js +// Add a breadcrumb +hawk.breadcrumbs.add(breadcrumb, hint); + +// Get current breadcrumbs +const breadcrumbs = hawk.breadcrumbs.get(); + +// Clear all breadcrumbs +hawk.breadcrumbs.clear(); +``` + ## Source maps consuming If your bundle is minified, it is useful to pass source-map files to the Hawk. After that you will see beautiful diff --git a/packages/javascript/example/breadcrumbs-tests.js b/packages/javascript/example/breadcrumbs-tests.js new file mode 100644 index 0000000..96124a7 --- /dev/null +++ b/packages/javascript/example/breadcrumbs-tests.js @@ -0,0 +1,306 @@ +/** + * Breadcrumbs Management + */ +const buttonAddBreadcrumb = document.getElementById('btn-add-breadcrumb'); +const buttonGetBreadcrumbs = document.getElementById('btn-get-breadcrumbs'); +const buttonClearBreadcrumbs = document.getElementById('btn-clear-breadcrumbs'); +const breadcrumbsOutput = document.getElementById('breadcrumbs-output'); + +buttonAddBreadcrumb.addEventListener('click', () => { + const message = document.getElementById('breadcrumbMessage').value; + const type = document.getElementById('breadcrumbType').value; + const level = document.getElementById('breadcrumbLevel').value; + const category = document.getElementById('breadcrumbCategory').value; + + if (!message.trim()) { + alert('Breadcrumb message is required'); + + return; + } + + window.hawk.breadcrumbs.add({ + message, + type, + level, + ...(category.trim() && { category }), + data: { + timestamp: new Date().toISOString(), + custom: 'manual breadcrumb', + }, + }); + + breadcrumbsOutput.textContent = `✓ Breadcrumb added: ${message}`; +}); + +buttonGetBreadcrumbs.addEventListener('click', () => { + const breadcrumbs = window.hawk.breadcrumbs.get(); + + if (breadcrumbs.length === 0) { + breadcrumbsOutput.textContent = 'No breadcrumbs yet'; + + return; + } + + breadcrumbsOutput.textContent = JSON.stringify(breadcrumbs, null, 2); +}); + +buttonClearBreadcrumbs.addEventListener('click', () => { + window.hawk.breadcrumbs.clear(); + breadcrumbsOutput.textContent = '✓ Breadcrumbs cleared'; +}); + +/** + * Test All Breadcrumb Types + */ +const buttonTestDefault = document.getElementById('btn-test-default'); +const buttonTestRequest = document.getElementById('btn-test-request'); +const buttonTestUI = document.getElementById('btn-test-ui'); +const buttonTestNavigation = document.getElementById('btn-test-navigation'); +const buttonTestLogic = document.getElementById('btn-test-logic'); +const buttonTestError = document.getElementById('btn-test-error'); +const buttonTestAllTypes = document.getElementById('btn-test-all-types'); + +/** + * Test Default breadcrumb (manual) + */ +buttonTestDefault.addEventListener('click', () => { + /** + * Default breadcrumbs are always added manually via hawk.breadcrumbs.add() + */ + window.hawk.breadcrumbs.add({ + type: 'default', + level: 'info', + category: 'user.action', + message: 'User clicked on default event button', + data: { + action: 'button_click', + context: 'breadcrumb_testing', + }, + }); + breadcrumbsOutput.textContent = '✓ Default breadcrumb added manually'; +}); + +/** + * Test Request breadcrumb (automatic via fetch) + */ +buttonTestRequest.addEventListener('click', async () => { + breadcrumbsOutput.textContent = 'Testing request breadcrumb...'; + + try { + const response = await fetch('https://api.github.com/zen'); + const text = await response.text(); + + breadcrumbsOutput.textContent = `✓ Request breadcrumb added (${response.status}): "${text}"`; + } catch (error) { + breadcrumbsOutput.textContent = `✗ Request failed: ${error.message}`; + } +}); + +/** + * Test UI breadcrumb (automatic tracking) + */ +buttonTestUI.addEventListener('click', () => { + /** + * Create a test element and click it to trigger automatic UI breadcrumb + * BreadcrumbManager automatically captures click events when trackClicks: true + */ + const testElement = document.createElement('button'); + + testElement.id = 'auto-click-test'; + testElement.className = 'test-button'; + testElement.textContent = 'Auto Test'; + testElement.style.position = 'absolute'; + testElement.style.opacity = '0'; + testElement.style.pointerEvents = 'none'; + document.body.appendChild(testElement); + + /** + * Trigger a click event + */ + testElement.click(); + + /** + * Clean up + */ + setTimeout(() => { + document.body.removeChild(testElement); + }, 100); + + breadcrumbsOutput.textContent = '✓ UI Click breadcrumb added automatically'; +}); + +/** + * Test Navigation breadcrumb (automatic tracking) + */ +buttonTestNavigation.addEventListener('click', () => { + /** + * Change the hash to trigger automatic navigation breadcrumb + * BreadcrumbManager automatically captures this event + */ + window.location.hash = 'breadcrumb-test-' + Date.now(); + + breadcrumbsOutput.textContent = '✓ Navigation breadcrumb added automatically'; +}); + +/** + * Test Logic breadcrumb (manual) + */ +buttonTestLogic.addEventListener('click', () => { + /** + * Simulate some logic operations + */ + const startTime = performance.now(); + + /** + * Complex calculation for testing + * + * @param {number} n - Number of iterations + * @returns {number} Calculation result + */ + function complexCalculation(n) { + let result = 0; + + for (let i = 0; i < n; i++) { + result += Math.sqrt(i); + } + + return result; + } + + const result = complexCalculation(10000); + const duration = performance.now() - startTime; + + /** + * Logic breadcrumbs are always added manually to track application flow + */ + window.hawk.breadcrumbs.add({ + type: 'logic', + level: 'debug', + category: 'calculation.complex', + message: 'Performed complex calculation', + data: { + operation: 'complexCalculation', + iterations: 10000, + result: result, + durationMs: duration.toFixed(2), + }, + }); + + breadcrumbsOutput.textContent = `✓ Logic breadcrumb added manually (${duration.toFixed(2)}ms)`; +}); + +/** + * Test Error breadcrumb (manual) + */ +buttonTestError.addEventListener('click', () => { + try { + /** + * Intentionally cause an error but catch it + */ + JSON.parse('invalid json {{{'); + } catch (error) { + /** + * Caught errors can be manually added as breadcrumbs + * Uncaught errors are sent to Hawk automatically, not as breadcrumbs + */ + window.hawk.breadcrumbs.add({ + type: 'error', + level: 'error', + category: 'json.parse', + message: `JSON parse error: ${error.message}`, + data: { + error: error.name, + message: error.message, + input: 'invalid json {{{', + }, + }); + + breadcrumbsOutput.textContent = `✓ Error breadcrumb added manually: ${error.message}`; + } +}); + +/** + * Test All Types in sequence + */ +buttonTestAllTypes.addEventListener('click', async () => { + breadcrumbsOutput.textContent = 'Running all breadcrumb types...'; + + /** + * 1. Default + */ + window.hawk.breadcrumbs.add({ + type: 'default', + level: 'info', + message: 'Sequence started', + }); + + await new Promise(resolve => setTimeout(resolve, 200)); + + /** + * 2. Logic + */ + window.hawk.breadcrumbs.add({ + type: 'logic', + level: 'debug', + category: 'sequence.step', + message: 'Processing step 1', + data: { + step: 1, + }, + }); + + await new Promise(resolve => setTimeout(resolve, 200)); + + /** + * 3. UI (automatic) + */ + const autoClickElement = document.createElement('button'); + + autoClickElement.id = 'sequence-auto-click'; + autoClickElement.style.position = 'absolute'; + autoClickElement.style.opacity = '0'; + autoClickElement.style.pointerEvents = 'none'; + document.body.appendChild(autoClickElement); + autoClickElement.click(); + document.body.removeChild(autoClickElement); + + await new Promise(resolve => setTimeout(resolve, 200)); + + /** + * 4. Request (automatic) + */ + try { + await fetch('https://api.github.com/zen'); + } catch (error) { + /** + * Fetch will be captured automatically + */ + } + + await new Promise(resolve => setTimeout(resolve, 200)); + + /** + * 5. Navigation (automatic) + */ + window.location.hash = 'sequence-test-' + Date.now(); + + await new Promise(resolve => setTimeout(resolve, 200)); + + /** + * 6. Error + */ + try { + throw new Error('Test error in sequence'); + } catch (error) { + window.hawk.breadcrumbs.add({ + type: 'error', + level: 'warning', + message: `Caught error: ${error.message}`, + data: { + error: error.name, + }, + }); + } + + breadcrumbsOutput.textContent = '✓ All breadcrumb types added! Check "Get Breadcrumbs"'; +}); diff --git a/packages/javascript/example/index.html b/packages/javascript/example/index.html index 7b6f125..1e8aefe 100644 --- a/packages/javascript/example/index.html +++ b/packages/javascript/example/index.html @@ -174,6 +174,50 @@