-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
13 changed files
with
700 additions
and
0 deletions.
There are no files selected for viewing
142 changes: 142 additions & 0 deletions
142
packages/telemetry/browser-telemetry/__tests__/collectors/http/fetch.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import { HttpBreadcrumb } from '../../../src/api/Breadcrumb'; | ||
import { Recorder } from '../../../src/api/Recorder'; | ||
import FetchCollector from '../../../src/collectors/http/fetch'; | ||
|
||
const initialFetch = window.fetch; | ||
|
||
describe('given a FetchCollector with a mock recorder', () => { | ||
let mockRecorder: Recorder; | ||
let collector: FetchCollector; | ||
|
||
beforeEach(() => { | ||
// Create mock recorder | ||
mockRecorder = { | ||
addBreadcrumb: jest.fn(), | ||
captureError: jest.fn(), | ||
captureErrorEvent: jest.fn(), | ||
}; | ||
// Create collector with default options | ||
collector = new FetchCollector({ | ||
urlFilters: [], // Add required urlFilters property | ||
}); | ||
}); | ||
|
||
it('registers recorder and uses it for fetch calls', async () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockResponse = new Response('test response', { status: 200, statusText: 'OK' }); | ||
(initialFetch as jest.Mock).mockResolvedValue(mockResponse); | ||
|
||
await fetch('https://api.example.com/data', { | ||
method: 'POST', | ||
body: JSON.stringify({ test: true }), | ||
}); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<HttpBreadcrumb>({ | ||
class: 'http', | ||
type: 'fetch', | ||
level: 'info', | ||
timestamp: expect.any(Number), | ||
data: { | ||
method: 'POST', | ||
url: 'https://api.example.com/data', | ||
statusCode: 200, | ||
statusText: 'OK', | ||
}, | ||
}), | ||
); | ||
}); | ||
|
||
it('stops adding breadcrumbs after unregistering', async () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
collector.unregister(); | ||
|
||
const mockResponse = new Response('test response', { status: 200, statusText: 'OK' }); | ||
(initialFetch as jest.Mock).mockResolvedValue(mockResponse); | ||
|
||
await fetch('https://api.example.com/data'); | ||
|
||
expect(mockRecorder.addBreadcrumb).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('filters URLs based on provided options', async () => { | ||
collector = new FetchCollector({ | ||
urlFilters: [(url: string) => url.replace(/token=.*/, 'token=REDACTED')], // Convert urlFilter to urlFilters array | ||
}); | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockResponse = new Response('test response', { status: 200, statusText: 'OK' }); | ||
(initialFetch as jest.Mock).mockResolvedValue(mockResponse); | ||
|
||
await fetch('https://api.example.com/data?token=secret123'); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<HttpBreadcrumb>({ | ||
data: { | ||
method: 'GET', | ||
url: 'https://api.example.com/data?token=REDACTED', | ||
statusCode: 200, | ||
statusText: 'OK', | ||
}, | ||
class: 'http', | ||
timestamp: expect.any(Number), | ||
level: 'info', | ||
type: 'fetch', | ||
}), | ||
); | ||
}); | ||
|
||
it('handles fetch calls with Request objects', async () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockResponse = new Response('test response', { status: 200, statusText: 'OK' }); | ||
(initialFetch as jest.Mock).mockResolvedValue(mockResponse); | ||
|
||
const request = new Request('https://api.example.com/data', { | ||
method: 'PUT', | ||
body: JSON.stringify({ test: true }), | ||
}); | ||
await fetch(request); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<HttpBreadcrumb>({ | ||
data: { | ||
method: 'PUT', | ||
url: 'https://api.example.com/data', | ||
statusCode: 200, | ||
statusText: 'OK', | ||
}, | ||
class: 'http', | ||
timestamp: expect.any(Number), | ||
level: 'info', | ||
type: 'fetch', | ||
}), | ||
); | ||
}); | ||
|
||
it('handles fetch calls with URL objects', async () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockResponse = new Response('test response', { status: 200, statusText: 'OK' }); | ||
(initialFetch as jest.Mock).mockResolvedValue(mockResponse); | ||
|
||
const url = new URL('https://api.example.com/data'); | ||
await fetch(url); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<HttpBreadcrumb>({ | ||
data: { | ||
method: 'GET', | ||
url: 'https://api.example.com/data', | ||
statusCode: 200, | ||
statusText: 'OK', | ||
}, | ||
class: 'http', | ||
timestamp: expect.any(Number), | ||
level: 'info', | ||
type: 'fetch', | ||
}), | ||
); | ||
}); | ||
}); |
139 changes: 139 additions & 0 deletions
139
packages/telemetry/browser-telemetry/__tests__/collectors/http/xhr.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import { HttpBreadcrumb } from '../../../src/api/Breadcrumb'; | ||
import { Recorder } from '../../../src/api/Recorder'; | ||
import XhrCollector from '../../../src/collectors/http/xhr'; | ||
|
||
const initialXhr = window.XMLHttpRequest; | ||
|
||
it('registers recorder and uses it for xhr calls', () => { | ||
const mockRecorder: Recorder = { | ||
addBreadcrumb: jest.fn(), | ||
captureError: jest.fn(), | ||
captureErrorEvent: jest.fn(), | ||
}; | ||
|
||
const collector = new XhrCollector({ | ||
urlFilters: [], | ||
}); | ||
|
||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const xhr = new XMLHttpRequest(); | ||
xhr.open('POST', 'https://api.example.com/data'); | ||
xhr.send(JSON.stringify({ test: true })); | ||
|
||
// Simulate successful response | ||
Object.defineProperty(xhr, 'status', { value: 200 }); | ||
Object.defineProperty(xhr, 'statusText', { value: 'OK' }); | ||
xhr.dispatchEvent(new Event('loadend')); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<HttpBreadcrumb>({ | ||
class: 'http', | ||
type: 'xhr', | ||
level: 'info', | ||
timestamp: expect.any(Number), | ||
data: { | ||
method: 'POST', | ||
url: 'https://api.example.com/data', | ||
statusCode: 200, | ||
statusText: 'OK', | ||
}, | ||
}), | ||
); | ||
}); | ||
|
||
it('stops adding breadcrumbs after unregistering', () => { | ||
const mockRecorder: Recorder = { | ||
addBreadcrumb: jest.fn(), | ||
captureError: jest.fn(), | ||
captureErrorEvent: jest.fn(), | ||
}; | ||
|
||
const collector = new XhrCollector({ | ||
urlFilters: [], | ||
}); | ||
|
||
collector.register(mockRecorder, 'test-session'); | ||
collector.unregister(); | ||
|
||
const xhr = new XMLHttpRequest(); | ||
xhr.open('GET', 'https://api.example.com/data'); | ||
xhr.send(); | ||
|
||
xhr.dispatchEvent(new Event('loadend')); | ||
|
||
expect(mockRecorder.addBreadcrumb).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('marks requests with error events as errors', () => { | ||
const mockRecorder: Recorder = { | ||
addBreadcrumb: jest.fn(), | ||
captureError: jest.fn(), | ||
captureErrorEvent: jest.fn(), | ||
}; | ||
|
||
const collector = new XhrCollector({ | ||
urlFilters: [], | ||
}); | ||
|
||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const xhr = new XMLHttpRequest(); | ||
xhr.open('GET', 'https://api.example.com/data'); | ||
xhr.send(); | ||
|
||
xhr.dispatchEvent(new Event('error')); | ||
xhr.dispatchEvent(new Event('loadend')); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<HttpBreadcrumb>({ | ||
level: 'error', | ||
data: expect.objectContaining({ | ||
method: 'GET', | ||
statusCode: 0, | ||
statusText: '', | ||
url: 'https://api.example.com/data', | ||
}), | ||
class: 'http', | ||
timestamp: expect.any(Number), | ||
type: 'xhr', | ||
}), | ||
); | ||
}); | ||
|
||
it('applies URL filters to requests', () => { | ||
const mockRecorder: Recorder = { | ||
addBreadcrumb: jest.fn(), | ||
captureError: jest.fn(), | ||
captureErrorEvent: jest.fn(), | ||
}; | ||
|
||
const collector = new XhrCollector({ | ||
urlFilters: [(url) => url.replace(/token=.*/, 'token=REDACTED')], | ||
}); | ||
|
||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const xhr = new XMLHttpRequest(); | ||
xhr.open('GET', 'https://api.example.com/data?token=secret123'); | ||
xhr.send(); | ||
|
||
Object.defineProperty(xhr, 'status', { value: 200 }); | ||
xhr.dispatchEvent(new Event('loadend')); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<HttpBreadcrumb>({ | ||
data: expect.objectContaining({ | ||
url: 'https://api.example.com/data?token=REDACTED', | ||
}), | ||
class: 'http', | ||
timestamp: expect.any(Number), | ||
level: 'info', | ||
type: 'xhr', | ||
}), | ||
); | ||
}); | ||
|
||
afterEach(() => { | ||
window.XMLHttpRequest = initialXhr; | ||
}); |
41 changes: 41 additions & 0 deletions
41
packages/telemetry/browser-telemetry/__tests__/filters/defaultUrlFilter.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import defaultUrlFilter from '../../src/filters/defaultUrlFilter'; | ||
|
||
it('filters polling urls', () => { | ||
// Added -_ to the end as we use those in the base64 URL safe character set. | ||
const context = | ||
'eyJraW5kIjoibXVsdGkiLCJ1c2VyIjp7ImtleSI6ImJvYiJ9LCJvcmciOnsia2V5IjoidGFjb2h1dCJ9fQ-_'; | ||
const filteredCotext = | ||
'************************************************************************************'; | ||
const baseUrl = 'https://sdk.launchdarkly.com/sdk/evalx/thesdkkey/contexts/'; | ||
const filteredUrl = `${baseUrl}${filteredCotext}`; | ||
const testUrl = `${baseUrl}${context}`; | ||
const testUrlWithReasons = `${testUrl}?withReasons=true`; | ||
const filteredUrlWithReasons = `${filteredUrl}?withReasons=true`; | ||
|
||
expect(defaultUrlFilter(testUrl)).toBe(filteredUrl); | ||
expect(defaultUrlFilter(testUrlWithReasons)).toBe(filteredUrlWithReasons); | ||
}); | ||
|
||
it('filters streaming urls', () => { | ||
// Added -_ to the end as we use those in the base64 URL safe character set. | ||
const context = | ||
'eyJraW5kIjoibXVsdGkiLCJ1c2VyIjp7ImtleSI6ImJvYiJ9LCJvcmciOnsia2V5IjoidGFjb2h1dCJ9fQ-_'; | ||
const filteredCotext = | ||
'************************************************************************************'; | ||
const baseUrl = `https://clientstream.launchdarkly.com/eval/thesdkkey/`; | ||
const filteredUrl = `${baseUrl}${filteredCotext}`; | ||
const testUrl = `${baseUrl}${context}`; | ||
const testUrlWithReasons = `${testUrl}?withReasons=true`; | ||
const filteredUrlWithReasons = `${filteredUrl}?withReasons=true`; | ||
|
||
expect(defaultUrlFilter(testUrl)).toBe(filteredUrl); | ||
expect(defaultUrlFilter(testUrlWithReasons)).toBe(filteredUrlWithReasons); | ||
}); | ||
|
||
it.each([ | ||
'http://events.launchdarkly.com/events/bulk/thesdkkey', | ||
'http://localhost:8080', | ||
'http://some.other.base64like/eyJraW5kIjoibXVsdGkiLCJ1c2VyIjp7ImtleSI6vcmciOnsiaIjoidGFjb2h1dCJ9fQ-_', | ||
])('passes through other URLs unfiltered', (url) => { | ||
expect(defaultUrlFilter(url)).toBe(url); | ||
}); |
21 changes: 21 additions & 0 deletions
21
packages/telemetry/browser-telemetry/__tests__/filters/filterHttpBreadcrumb.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { HttpBreadcrumb } from '../../src/api/Breadcrumb'; | ||
import filterHttpBreadcrumb from '../../src/filters/filterHttpBreadcrumb'; | ||
|
||
it('filters breadcrumbs with the provided filters', () => { | ||
const breadcrumb: HttpBreadcrumb = { | ||
class: 'http', | ||
timestamp: Date.now(), | ||
level: 'info', | ||
type: 'xhr', | ||
data: { | ||
method: 'GET', | ||
url: 'dog', | ||
statusCode: 200, | ||
statusText: 'ok', | ||
}, | ||
}; | ||
filterHttpBreadcrumb(breadcrumb, { | ||
urlFilters: [(url) => url.replace('dog', 'cat')], | ||
}); | ||
expect(breadcrumb.data?.url).toBe('cat'); | ||
}); |
13 changes: 13 additions & 0 deletions
13
packages/telemetry/browser-telemetry/__tests__/filters/filterUrl.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import filterUrl from '../../src/filters/filterUrl'; | ||
|
||
it('runs the specified filters in the given order', () => { | ||
const filterA = (url: string): string => url.replace('dog', 'cat'); | ||
const filterB = (url: string): string => url.replace('cat', 'mouse'); | ||
|
||
// dog -> cat -> mouse | ||
expect(filterUrl([filterA, filterB], 'dog')).toBe('mouse'); | ||
// dog -> dog -> cat | ||
expect(filterUrl([filterB, filterA], 'dog')).toBe('cat'); | ||
// cat -> mouse -> mouse | ||
expect(filterUrl([filterB, filterA], 'cat')).toBe('mouse'); | ||
}); |
13 changes: 13 additions & 0 deletions
13
packages/telemetry/browser-telemetry/src/collectors/http/HttpCollectorOptions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { UrlFilter } from '../../api/Options'; | ||
|
||
/** | ||
* Options which impact the behavior of http collectors. | ||
*/ | ||
export default interface HttpCollectorOptions { | ||
/** | ||
* A list of filters to execute on the URL of the breadcrumb. | ||
* | ||
* This allows for redaction of potentially sensitive information in URLs. | ||
*/ | ||
urlFilters: UrlFilter[]; | ||
} |
Oops, something went wrong.