From f998c529b616fea9937cad3cf98a1d470cc7da94 Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Thu, 21 Nov 2024 16:49:02 +0000 Subject: [PATCH 01/10] add sendPayloadChecksums config option and implement Bugsnag-Integrity header --- .tool-versions | 1 + jest.config.js | 4 + jest/setup/crypto.ts | 19 +++ packages/core/lib/config.ts | 9 +- packages/core/lib/core.ts | 7 +- packages/core/lib/delivery.ts | 4 +- packages/core/tests/core.test.ts | 4 +- packages/delivery-fetch/lib/delivery.ts | 29 +++- .../delivery-fetch/tests/delivery.test.ts | 142 ++++++++++++++++-- .../platforms/browser/tests/browser.test.ts | 69 +++++++++ 10 files changed, 263 insertions(+), 25 deletions(-) create mode 100644 .tool-versions create mode 100644 jest/setup/crypto.ts diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..d7568adf6 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 20.11.1 diff --git a/jest.config.js b/jest.config.js index 228ed1f67..19fd30d22 100644 --- a/jest.config.js +++ b/jest.config.js @@ -46,11 +46,15 @@ module.exports = { { displayName: 'delivery-fetch', testMatch: ['/packages/delivery-fetch/**/*.test.ts'], + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/jest/setup/crypto.ts'], ...defaultModuleConfig }, { displayName: 'browser', testMatch: ['/packages/platforms/browser/**/*.test.ts'], + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/jest/setup/crypto.ts'], ...defaultModuleConfig }, { diff --git a/jest/setup/crypto.ts b/jest/setup/crypto.ts new file mode 100644 index 000000000..659f09583 --- /dev/null +++ b/jest/setup/crypto.ts @@ -0,0 +1,19 @@ +import { TextDecoder, TextEncoder } from 'node:util' +import crypto from 'crypto' + +Object.defineProperty(window, 'crypto', { + get () { + return { + getRandomValues: crypto.getRandomValues, + subtle: crypto.webcrypto.subtle + } + } +}) + +Object.defineProperty(window, 'TextEncoder', { + get () { return TextEncoder } +}) + +Object.defineProperty(window, 'TextDecoder', { + get () { return TextDecoder } +}) diff --git a/packages/core/lib/config.ts b/packages/core/lib/config.ts index ebf3abca7..a6fd24de5 100644 --- a/packages/core/lib/config.ts +++ b/packages/core/lib/config.ts @@ -8,7 +8,7 @@ import { ATTRIBUTE_STRING_VALUE_LIMIT_MAX } from './custom-attribute-limits' import type { Plugin } from './plugin' -import { isLogger, isNumber, isObject, isOnSpanEndCallbacks, isPluginArray, isString, isStringArray, isStringWithLength } from './validation' +import { isBoolean, isLogger, isNumber, isObject, isOnSpanEndCallbacks, isPluginArray, isString, isStringArray, isStringWithLength } from './validation' type SetTraceCorrelation = (traceId: string, spanId: string) => void @@ -53,6 +53,7 @@ export interface Configuration { attributeStringValueLimit?: number attributeArrayLengthLimit?: number attributeCountLimit?: number + sendPayloadChecksums?: boolean } export interface TestConfiguration { @@ -81,6 +82,7 @@ export interface CoreSchema extends Schema { plugins: ConfigOption>> bugsnag: ConfigOption samplingProbability: ConfigOption + sendPayloadChecksums: ConfigOption } export const schema: CoreSchema = { @@ -153,6 +155,11 @@ export const schema: CoreSchema = { defaultValue: ATTRIBUTE_COUNT_LIMIT_DEFAULT, message: `should be a number between 1 and ${ATTRIBUTE_COUNT_LIMIT_MAX}`, validate: (value: unknown): value is number => isNumber(value) && value > 0 && value <= ATTRIBUTE_COUNT_LIMIT_MAX + }, + sendPayloadChecksums: { + defaultValue: false, + message: 'should be true|false', + validate: isBoolean } } diff --git a/packages/core/lib/core.ts b/packages/core/lib/core.ts index f259e35ea..ed74a0e8c 100644 --- a/packages/core/lib/core.ts +++ b/packages/core/lib/core.ts @@ -71,6 +71,11 @@ export function createClient ( return { start: (config: C | string) => { + // sendPayloadChecksums is false by default unless custom endpoints are not specified + if (typeof config !== 'string' && !config.endpoint) { + config.sendPayloadChecksums = 'sendPayloadChecksums' in config ? config.sendPayloadChecksums : true + } + const configuration = validateConfig(config, options.schema) // if using the default endpoint add the API key as a subdomain @@ -92,7 +97,7 @@ export function createClient ( } } - const delivery = options.deliveryFactory(configuration.endpoint) + const delivery = options.deliveryFactory(configuration.endpoint, configuration.sendPayloadChecksums) options.spanAttributesSource.configure(configuration) diff --git a/packages/core/lib/delivery.ts b/packages/core/lib/delivery.ts index 1bdecf39d..036f4a25a 100644 --- a/packages/core/lib/delivery.ts +++ b/packages/core/lib/delivery.ts @@ -5,7 +5,7 @@ import type { JsonEvent } from './events' import type { Kind, SpanEnded } from './span' import { spanToJson } from './span' -export type DeliveryFactory = (endpoint: string) => Delivery +export type DeliveryFactory = (endpoint: string, sendPayloadChecksums: boolean) => Delivery export type ResponseState = 'success' | 'failure-discard' | 'failure-retryable' @@ -60,6 +60,8 @@ export interface TracePayload { // therefore it's 'undefined' when passed to delivery, which adds a value // immediately before initiating the request 'Bugsnag-Sent-At'?: string + // 'undefined' when passed to delivery, which adds a value before initiating the request + 'Bugsnag-Integrity'?: string } } diff --git a/packages/core/tests/core.test.ts b/packages/core/tests/core.test.ts index 258c915ae..3a20d196c 100644 --- a/packages/core/tests/core.test.ts +++ b/packages/core/tests/core.test.ts @@ -342,7 +342,7 @@ describe('Core', () => { await jest.runOnlyPendingTimersAsync() - expect(deliveryFactory).toHaveBeenCalledWith('https://a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.otlp.bugsnag.com/v1/traces') + expect(deliveryFactory).toHaveBeenCalledWith('https://a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.otlp.bugsnag.com/v1/traces', false) }) }) @@ -356,7 +356,7 @@ describe('Core', () => { await jest.runOnlyPendingTimersAsync() - expect(deliveryFactory).toHaveBeenCalledWith('https://my-custom-otel-repeater.com') + expect(deliveryFactory).toHaveBeenCalledWith('https://my-custom-otel-repeater.com', false) }) }) }) diff --git a/packages/delivery-fetch/lib/delivery.ts b/packages/delivery-fetch/lib/delivery.ts index 69a638b27..600ac3b11 100644 --- a/packages/delivery-fetch/lib/delivery.ts +++ b/packages/delivery-fetch/lib/delivery.ts @@ -1,7 +1,4 @@ -import { - - responseStateFromStatusCode -} from '@bugsnag/core-performance' +import { responseStateFromStatusCode } from '@bugsnag/core-performance' import type { BackgroundingListener, Clock, Delivery, DeliveryFactory, TracePayload } from '@bugsnag/core-performance' export type Fetch = typeof fetch @@ -40,7 +37,7 @@ function createFetchDeliveryFactory ( }) } - return function fetchDeliveryFactory (endpoint: string): Delivery { + return function fetchDeliveryFactory (endpoint: string, sendPayloadChecksums?: boolean): Delivery { return { async send (payload: TracePayload) { const body = JSON.stringify(payload.body) @@ -48,6 +45,12 @@ function createFetchDeliveryFactory ( payload.headers['Bugsnag-Sent-At'] = clock.date().toISOString() try { + const integrityHeaderValue = await getIntegrityHeaderValue(sendPayloadChecksums ?? false, window, body) + + if (integrityHeaderValue) { + payload.headers['Bugsnag-Integrity'] = integrityHeaderValue + } + const response = await fetch(endpoint, { method: 'POST', keepalive, @@ -72,3 +75,19 @@ function createFetchDeliveryFactory ( } export default createFetchDeliveryFactory + +function getIntegrityHeaderValue (sendPayloadChecksums: boolean, windowOrWorkerGlobalScope: Window, requestBody: string) { + if (sendPayloadChecksums && windowOrWorkerGlobalScope.isSecureContext && windowOrWorkerGlobalScope.crypto && windowOrWorkerGlobalScope.crypto.subtle && windowOrWorkerGlobalScope.crypto.subtle.digest && typeof TextEncoder === 'function') { + const msgUint8 = new TextEncoder().encode(requestBody) + return windowOrWorkerGlobalScope.crypto.subtle.digest('SHA-1', msgUint8).then((hashBuffer) => { + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join('') + + return 'sha1 ' + hashHex + }) + } + + return Promise.resolve() +} diff --git a/packages/delivery-fetch/tests/delivery.test.ts b/packages/delivery-fetch/tests/delivery.test.ts index be8e64687..50b7d698c 100644 --- a/packages/delivery-fetch/tests/delivery.test.ts +++ b/packages/delivery-fetch/tests/delivery.test.ts @@ -1,7 +1,3 @@ -/** - * @jest-environment jsdom - */ - import type { TracePayload } from '@bugsnag/core-performance' import type { JsonEvent } from '@bugsnag/core-performance/lib' import { @@ -14,11 +10,15 @@ import createFetchDeliveryFactory from '../lib/delivery' const SENT_AT_FORMAT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ describe('Browser Delivery', () => { - it('delivers a span', async () => { - const fetch = jest.fn(() => Promise.resolve({ status: 200, headers: new Headers() } as unknown as Response)) - const backgroundingListener = new ControllableBackgroundingListener() - const clock = new IncrementingClock('2023-01-02T00:00:00.000Z') + beforeAll(() => { + window.isSecureContext = true + }) + + afterAll(() => { + window.isSecureContext = false + }) + it('delivers a span', async () => { const deliveryPayload: TracePayload = { body: { resourceSpans: [{ @@ -45,8 +45,12 @@ describe('Browser Delivery', () => { } } + const fetch = jest.fn(() => Promise.resolve({ status: 200, headers: new Headers() } as unknown as Response)) + const backgroundingListener = new ControllableBackgroundingListener() + const clock = new IncrementingClock('2023-01-02T00:00:00.000Z') + const deliveryFactory = createFetchDeliveryFactory(fetch, clock, backgroundingListener) - const delivery = deliveryFactory('/test') + const delivery = deliveryFactory('/test', true) const response = await delivery.send(deliveryPayload) expect(fetch).toHaveBeenCalledWith('/test', { @@ -57,7 +61,8 @@ describe('Browser Delivery', () => { 'Bugsnag-Api-Key': 'test-api-key', 'Bugsnag-Span-Sampling': '1:1', 'Content-Type': 'application/json', - 'Bugsnag-Sent-At': new Date(clock.timeOrigin + 1).toISOString() + 'Bugsnag-Sent-At': new Date(clock.timeOrigin + 1).toISOString(), + 'Bugsnag-Integrity': 'sha1 835f1ff603eb1be3bf91fc9716c0841e1b37ee64' } }) @@ -98,7 +103,7 @@ describe('Browser Delivery', () => { } const deliveryFactory = createFetchDeliveryFactory(fetch, new IncrementingClock(), backgroundingListener) - const delivery = deliveryFactory('/test') + const delivery = deliveryFactory('/test', false) backgroundingListener.sendToBackground() @@ -153,7 +158,7 @@ describe('Browser Delivery', () => { } const deliveryFactory = createFetchDeliveryFactory(fetch, new IncrementingClock(), backgroundingListener) - const delivery = deliveryFactory('/test') + const delivery = deliveryFactory('/test', false) backgroundingListener.sendToBackground() backgroundingListener.sendToForeground() @@ -200,7 +205,7 @@ describe('Browser Delivery', () => { const backgroundingListener = new ControllableBackgroundingListener() const deliveryFactory = createFetchDeliveryFactory(fetch, new IncrementingClock(), backgroundingListener) - const delivery = deliveryFactory('/test') + const delivery = deliveryFactory('/test', false) const deliveryPayload: TracePayload = { body: { resourceSpans: [] }, headers: { @@ -238,7 +243,7 @@ describe('Browser Delivery', () => { const backgroundingListener = new ControllableBackgroundingListener() const deliveryFactory = createFetchDeliveryFactory(fetch, new IncrementingClock(), backgroundingListener) - const delivery = deliveryFactory('/test') + const delivery = deliveryFactory('/test', false) const payload: TracePayload = { body: { resourceSpans: [] }, headers: { @@ -293,10 +298,117 @@ describe('Browser Delivery', () => { } const deliveryFactory = createFetchDeliveryFactory(fetch, clock, backgroundingListener) - const delivery = deliveryFactory('/test') + const delivery = deliveryFactory('/test', false) const { state } = await delivery.send(deliveryPayload) expect(state).toBe('failure-discard') }) + + it('omits the bugsnag integrity header when not in a secure context', async () => { + const deliveryPayload: TracePayload = { + body: { + resourceSpans: [{ + resource: { attributes: [{ key: 'test-key', value: { stringValue: 'test-value' } }] }, + scopeSpans: [{ + spans: [{ + name: 'test-span', + kind: 1, + spanId: 'test-span-id', + traceId: 'test-trace-id', + endTimeUnixNano: '56789', + startTimeUnixNano: '12345', + attributes: [{ key: 'test-span', value: { intValue: '12345' } }], + droppedAttributesCount: 0, + events: [] + }] + }] + }] + }, + headers: { + 'Bugsnag-Api-Key': 'test-api-key', + 'Content-Type': 'application/json', + 'Bugsnag-Span-Sampling': '1:1' + } + } + + window.isSecureContext = false + const _fetch = jest.fn(() => Promise.resolve({ status: 200, headers: new Headers() } as unknown as Response)) + const backgroundingListener = new ControllableBackgroundingListener() + const clock = new IncrementingClock('2023-01-02T00:00:00.000Z') + + const deliveryFactory = createFetchDeliveryFactory(_fetch, clock, backgroundingListener) + const delivery = deliveryFactory('/test', true) + const response = await delivery.send(deliveryPayload) + + expect(_fetch).toHaveBeenCalledWith('/test', { + method: 'POST', + keepalive: false, + body: JSON.stringify(deliveryPayload.body), + headers: { + 'Bugsnag-Api-Key': 'test-api-key', + 'Bugsnag-Span-Sampling': '1:1', + 'Content-Type': 'application/json', + 'Bugsnag-Sent-At': new Date(clock.timeOrigin + 1).toISOString() + } + }) + + expect(response).toStrictEqual({ + state: 'success', + samplingProbability: undefined + }) + }) + + it('omits the bugsnag integrity header when sendPayloadChecksums is false', async () => { + const deliveryPayload: TracePayload = { + body: { + resourceSpans: [{ + resource: { attributes: [{ key: 'test-key', value: { stringValue: 'test-value' } }] }, + scopeSpans: [{ + spans: [{ + name: 'test-span', + kind: 1, + spanId: 'test-span-id', + traceId: 'test-trace-id', + endTimeUnixNano: '56789', + startTimeUnixNano: '12345', + attributes: [{ key: 'test-span', value: { intValue: '12345' } }], + droppedAttributesCount: 0, + events: [] + }] + }] + }] + }, + headers: { + 'Bugsnag-Api-Key': 'test-api-key', + 'Content-Type': 'application/json', + 'Bugsnag-Span-Sampling': '1:1' + } + } + + const fetch = jest.fn(() => Promise.resolve({ status: 200, headers: new Headers() } as unknown as Response)) + const backgroundingListener = new ControllableBackgroundingListener() + const clock = new IncrementingClock('2023-01-02T00:00:00.000Z') + + const deliveryFactory = createFetchDeliveryFactory(fetch, clock, backgroundingListener) + const delivery = deliveryFactory('/test', false) + const response = await delivery.send(deliveryPayload) + + expect(fetch).toHaveBeenCalledWith('/test', { + method: 'POST', + keepalive: false, + body: JSON.stringify(deliveryPayload.body), + headers: { + 'Bugsnag-Api-Key': 'test-api-key', + 'Bugsnag-Span-Sampling': '1:1', + 'Content-Type': 'application/json', + 'Bugsnag-Sent-At': new Date(clock.timeOrigin + 1).toISOString() + } + }) + + expect(response).toStrictEqual({ + state: 'success', + samplingProbability: undefined + }) + }) }) diff --git a/packages/platforms/browser/tests/browser.test.ts b/packages/platforms/browser/tests/browser.test.ts index 8deab3243..502fbf193 100644 --- a/packages/platforms/browser/tests/browser.test.ts +++ b/packages/platforms/browser/tests/browser.test.ts @@ -319,4 +319,73 @@ describe('Browser client integration tests', () => { await jest.runOnlyPendingTimersAsync() }) }) + + describe('payload checksum behavior (Bugsnag-Integrity header)', () => { + beforeAll(() => { + // eslint-disable-next-line compat/compat + window.isSecureContext = true + }) + + afterAll(() => { + // eslint-disable-next-line compat/compat + window.isSecureContext = false + }) + + it('includes the integrity header by default', async () => { + client.start({ + apiKey: VALID_API_KEY, + autoInstrumentFullPageLoads: false, + autoInstrumentNetworkRequests: false, + autoInstrumentRouteChanges: false + }) + + setNextSamplingProbability(0.2) + createSpans(100) + await jest.advanceTimersByTimeAsync(RESPONSE_TIME + 1) + expect(mockFetch).toHaveBeenCalledTimes(2) + + expect(mockFetch.mock.calls[1][1]).toEqual(expect.objectContaining({ + headers: expect.objectContaining({ 'Bugsnag-Integrity': expect.stringMatching(/^sha1 (\d|[abcdef]){40}$/) }) + })) + }) + + it('does not include the integrity header if endpoint configuration is supplied', async () => { + client.start({ + apiKey: VALID_API_KEY, + endpoint: '/test', + autoInstrumentFullPageLoads: false, + autoInstrumentNetworkRequests: false, + autoInstrumentRouteChanges: false + }) + + setNextSamplingProbability(0.2) + createSpans(100) + await jest.advanceTimersByTimeAsync(RESPONSE_TIME + 1) + expect(mockFetch).toHaveBeenCalledTimes(2) + + expect(mockFetch.mock.calls[1][1]).toEqual(expect.objectContaining({ + headers: expect.not.objectContaining({ 'Bugsnag-Integrity': expect.any(String) }) + })) + }) + + it('can be enabled for a custom endpoint configuration by using sendPayloadChecksums', async () => { + client.start({ + apiKey: VALID_API_KEY, + endpoint: '/test', + sendPayloadChecksums: true, + autoInstrumentFullPageLoads: false, + autoInstrumentNetworkRequests: false, + autoInstrumentRouteChanges: false + }) + + setNextSamplingProbability(0.2) + createSpans(100) + await jest.advanceTimersByTimeAsync(RESPONSE_TIME + 1) + expect(mockFetch).toHaveBeenCalledTimes(2) + + expect(mockFetch.mock.calls[1][1]).toEqual(expect.objectContaining({ + headers: expect.objectContaining({ 'Bugsnag-Integrity': expect.stringMatching(/^sha1 (\d|[abcdef]){40}$/) }) + })) + }) + }) }) From b8f5d5b565f5bd295873acfa48adc6740c4304e3 Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Thu, 21 Nov 2024 17:11:44 +0000 Subject: [PATCH 02/10] add sendPayloadChecksums config option and implement Bugsnag-Integrity header --- packages/core/lib/core.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/lib/core.ts b/packages/core/lib/core.ts index ed74a0e8c..215618e5a 100644 --- a/packages/core/lib/core.ts +++ b/packages/core/lib/core.ts @@ -71,13 +71,13 @@ export function createClient ( return { start: (config: C | string) => { + const configuration = validateConfig(config, options.schema) + // sendPayloadChecksums is false by default unless custom endpoints are not specified if (typeof config !== 'string' && !config.endpoint) { - config.sendPayloadChecksums = 'sendPayloadChecksums' in config ? config.sendPayloadChecksums : true + configuration.sendPayloadChecksums = ('sendPayloadChecksums' in config && config.sendPayloadChecksums) || true } - const configuration = validateConfig(config, options.schema) - // if using the default endpoint add the API key as a subdomain // e.g. convert URL https://otlp.bugsnag.com/v1/traces to URL https://.otlp.bugsnag.com/v1/traces if (configuration.endpoint === schema.endpoint.defaultValue) { From 287bf9bbac30bfd4d7ac3dfd3a3793a1111d129e Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Tue, 26 Nov 2024 13:35:06 +0000 Subject: [PATCH 03/10] add integrity e2e test --- .../fixtures/packages/integrity/index.html | 13 +++++++++ .../fixtures/packages/integrity/package.json | 8 ++++++ .../fixtures/packages/integrity/src/index.js | 28 +++++++++++++++++++ test/browser/features/integrity.feature | 10 +++++++ test/browser/features/support/maze-config.rb | 6 +--- 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 test/browser/features/fixtures/packages/integrity/index.html create mode 100644 test/browser/features/fixtures/packages/integrity/package.json create mode 100644 test/browser/features/fixtures/packages/integrity/src/index.js create mode 100644 test/browser/features/integrity.feature diff --git a/test/browser/features/fixtures/packages/integrity/index.html b/test/browser/features/fixtures/packages/integrity/index.html new file mode 100644 index 000000000..65e6364f8 --- /dev/null +++ b/test/browser/features/fixtures/packages/integrity/index.html @@ -0,0 +1,13 @@ + + + + + + + Integrity header + + +

integrity-header

+ + + diff --git a/test/browser/features/fixtures/packages/integrity/package.json b/test/browser/features/fixtures/packages/integrity/package.json new file mode 100644 index 000000000..2c29397f5 --- /dev/null +++ b/test/browser/features/fixtures/packages/integrity/package.json @@ -0,0 +1,8 @@ +{ + "name": "integrity", + "private": true, + "scripts": { + "build": "rollup --config ../rollup.config.mjs", + "clean": "rm -rf dist" + } +} diff --git a/test/browser/features/fixtures/packages/integrity/src/index.js b/test/browser/features/fixtures/packages/integrity/src/index.js new file mode 100644 index 000000000..dab93ae96 --- /dev/null +++ b/test/browser/features/fixtures/packages/integrity/src/index.js @@ -0,0 +1,28 @@ +import BugsnagPerformance from '@bugsnag/browser-performance' + +const parameters = new URLSearchParams(window.location.search) +const apiKey = parameters.get('api_key') +const endpoint = parameters.get('endpoint') + +BugsnagPerformance.start({ + apiKey, + endpoint, + sendPayloadChecksums: true, + maximumBatchSize: 1, + autoInstrumentFullPageLoads: false, + autoInstrumentNetworkRequests: false, + autoInstrumentRouteChanges: false, + serviceName: 'integrity' +}) + +document.getElementById('send-span').onclick = () => { + const spanOptions = {} + + if (parameters.has('isFirstClass')) { + spanOptions.isFirstClass = JSON.parse(parameters.get('isFirstClass')) + } + + const span = BugsnagPerformance.startSpan("Custom/ManualSpanScenario", spanOptions) + span.end() +} + diff --git a/test/browser/features/integrity.feature b/test/browser/features/integrity.feature new file mode 100644 index 000000000..089f9aca1 --- /dev/null +++ b/test/browser/features/integrity.feature @@ -0,0 +1,10 @@ +Feature: Integrity header + +Scenario: Integrity headers are set when setPayloadChecksums is true + Given I navigate to the test URL "/docs/integrity" + And I wait to receive a sampling request + Then I click the element "send-span" + And I wait for 1 span + + Then the sampling request "bugsnag-integrity" header matches the regex "^sha1 (\d|[abcdef]){40}$" + Then the trace "bugsnag-integrity" header matches the regex "^sha1 (\d|[abcdef]){40}$" diff --git a/test/browser/features/support/maze-config.rb b/test/browser/features/support/maze-config.rb index a15b30b7b..c78fd19d9 100644 --- a/test/browser/features/support/maze-config.rb +++ b/test/browser/features/support/maze-config.rb @@ -16,11 +16,7 @@ def get_test_url() maze_address = "#{host}:9339" end - if Maze.config.https - protocol = 'https' - else - protocol = 'http' - end + protocol = Maze.config.https ? 'https' : 'http' UrlGenerator.new( URI("#{protocol}://#{maze_address}"), From 0ffe3fadd4c55de46aded5703365563f312ce4ac Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Tue, 26 Nov 2024 14:42:31 +0000 Subject: [PATCH 04/10] add test for integrity disabled --- .../packages/integrity-disabled/index.html | 13 +++++++++ .../packages/integrity-disabled/package.json | 8 ++++++ .../packages/integrity-disabled/src/index.js | 28 +++++++++++++++++++ test/browser/features/integrity.feature | 9 ++++++ 4 files changed, 58 insertions(+) create mode 100644 test/browser/features/fixtures/packages/integrity-disabled/index.html create mode 100644 test/browser/features/fixtures/packages/integrity-disabled/package.json create mode 100644 test/browser/features/fixtures/packages/integrity-disabled/src/index.js diff --git a/test/browser/features/fixtures/packages/integrity-disabled/index.html b/test/browser/features/fixtures/packages/integrity-disabled/index.html new file mode 100644 index 000000000..65e6364f8 --- /dev/null +++ b/test/browser/features/fixtures/packages/integrity-disabled/index.html @@ -0,0 +1,13 @@ + + + + + + + Integrity header + + +

integrity-header

+ + + diff --git a/test/browser/features/fixtures/packages/integrity-disabled/package.json b/test/browser/features/fixtures/packages/integrity-disabled/package.json new file mode 100644 index 000000000..2c29397f5 --- /dev/null +++ b/test/browser/features/fixtures/packages/integrity-disabled/package.json @@ -0,0 +1,8 @@ +{ + "name": "integrity", + "private": true, + "scripts": { + "build": "rollup --config ../rollup.config.mjs", + "clean": "rm -rf dist" + } +} diff --git a/test/browser/features/fixtures/packages/integrity-disabled/src/index.js b/test/browser/features/fixtures/packages/integrity-disabled/src/index.js new file mode 100644 index 000000000..13a2f8c8a --- /dev/null +++ b/test/browser/features/fixtures/packages/integrity-disabled/src/index.js @@ -0,0 +1,28 @@ +import BugsnagPerformance from '@bugsnag/browser-performance' + +const parameters = new URLSearchParams(window.location.search) +const apiKey = parameters.get('api_key') +const endpoint = parameters.get('endpoint') + +BugsnagPerformance.start({ + apiKey, + endpoint, + sendPayloadChecksums: false, + maximumBatchSize: 1, + autoInstrumentFullPageLoads: false, + autoInstrumentNetworkRequests: false, + autoInstrumentRouteChanges: false, + serviceName: 'integrity' +}) + +document.getElementById('send-span').onclick = () => { + const spanOptions = {} + + if (parameters.has('isFirstClass')) { + spanOptions.isFirstClass = JSON.parse(parameters.get('isFirstClass')) + } + + const span = BugsnagPerformance.startSpan("Custom/ManualSpanScenario", spanOptions) + span.end() +} + diff --git a/test/browser/features/integrity.feature b/test/browser/features/integrity.feature index 089f9aca1..867596ac1 100644 --- a/test/browser/features/integrity.feature +++ b/test/browser/features/integrity.feature @@ -8,3 +8,12 @@ Scenario: Integrity headers are set when setPayloadChecksums is true Then the sampling request "bugsnag-integrity" header matches the regex "^sha1 (\d|[abcdef]){40}$" Then the trace "bugsnag-integrity" header matches the regex "^sha1 (\d|[abcdef]){40}$" + +Scenario: Integrity headers are not set when setPayloadChecksums is false + Given I navigate to the test URL "/docs/integrity-disabled" + And I wait to receive a sampling request + Then I click the element "send-span" + And I wait for 1 span + + Then the sampling request "bugsnag-integrity" header is not present + Then the trace "bugsnag-integrity" header is not present \ No newline at end of file From 9860c08e93458d939e5b20c5e51492bbee5fee9a Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Tue, 26 Nov 2024 15:04:17 +0000 Subject: [PATCH 05/10] add test for integrity disabled --- .../features/fixtures/packages/integrity-disabled/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/features/fixtures/packages/integrity-disabled/package.json b/test/browser/features/fixtures/packages/integrity-disabled/package.json index 2c29397f5..62faf341f 100644 --- a/test/browser/features/fixtures/packages/integrity-disabled/package.json +++ b/test/browser/features/fixtures/packages/integrity-disabled/package.json @@ -1,5 +1,5 @@ { - "name": "integrity", + "name": "integrity-disabled", "private": true, "scripts": { "build": "rollup --config ../rollup.config.mjs", From 7b4654859610401fbdf74b02c18a60639b0c4269 Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Wed, 27 Nov 2024 10:09:18 +0000 Subject: [PATCH 06/10] disable --https for mobile browser tests --- .buildkite/browser-pipeline.full.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.buildkite/browser-pipeline.full.yml b/.buildkite/browser-pipeline.full.yml index 967c31e4e..577b55fc8 100644 --- a/.buildkite/browser-pipeline.full.yml +++ b/.buildkite/browser-pipeline.full.yml @@ -29,7 +29,6 @@ steps: run: browser-maze-runner-bs use-aliases: true command: - - --https - --farm=bs - --browser={{ matrix }} artifacts#v1.5.0: From d917aa0848f338ea6208fd784053140e4ba7aed7 Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Wed, 27 Nov 2024 10:09:34 +0000 Subject: [PATCH 07/10] skip integrity tests on safari 11 --- test/browser/features/integrity.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/test/browser/features/integrity.feature b/test/browser/features/integrity.feature index 867596ac1..8cca1ec94 100644 --- a/test/browser/features/integrity.feature +++ b/test/browser/features/integrity.feature @@ -1,3 +1,4 @@ +@skip_safari_11 Feature: Integrity header Scenario: Integrity headers are set when setPayloadChecksums is true From be449f45ae2ef4a2a59cdbbb0082dc1b2896e586 Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Wed, 27 Nov 2024 13:47:34 +0000 Subject: [PATCH 08/10] try fix CDN build --- test/browser/features/fixtures/packages/rollup.config.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/browser/features/fixtures/packages/rollup.config.mjs b/test/browser/features/fixtures/packages/rollup.config.mjs index 08449f4d5..479930f2d 100644 --- a/test/browser/features/fixtures/packages/rollup.config.mjs +++ b/test/browser/features/fixtures/packages/rollup.config.mjs @@ -10,8 +10,8 @@ export const isCdnBuild = process.env.USE_CDN_BUILD === "1" || process.env.USE_C const cdnOutputOptions = { // import BugsnagPerformance from the CDN build banner: process.env.DEBUG - ? 'import BugsnagPerformance from "/bugsnag-performance.js"\n' - : 'import BugsnagPerformance from "/bugsnag-performance.min.js"\n', + ? 'import BugsnagPerformance from "/docs/bugsnag-performance.js"\n' + : 'import BugsnagPerformance from "/docs/bugsnag-performance.min.js"\n', globals: { '@bugsnag/browser-performance': 'BugsnagPerformance', '@bugsnag/react-router-performance': 'BugsnagReactRouterPerformance', From 5542818332c6382dccd9b301f5a6cbc8a4d5523e Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Wed, 27 Nov 2024 14:18:36 +0000 Subject: [PATCH 09/10] try fix CDN build --- test/browser/features/resource-load-spans.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/features/resource-load-spans.feature b/test/browser/features/resource-load-spans.feature index 770a4690c..8003e4e62 100644 --- a/test/browser/features/resource-load-spans.feature +++ b/test/browser/features/resource-load-spans.feature @@ -52,7 +52,7 @@ Feature: Resource Load Spans And the trace payload field "resourceSpans.0.scopeSpans.0.spans.0" double attribute "bugsnag.sampling.p" equals 0.999999 # CDN bundle - And the trace payload field "resourceSpans.0.scopeSpans.0.spans.1.name" matches the regex "^\[ResourceLoad\]https:\/\/.*:[0-9]{4}\/bugsnag-performance(?:\.min)?\.js$" + And the trace payload field "resourceSpans.0.scopeSpans.0.spans.1.name" matches the regex "^\[ResourceLoad\]https:\/\/.*:[0-9]{4}\/docs\/bugsnag-performance(?:\.min)?\.js$" And the trace payload field "resourceSpans.0.scopeSpans.0.spans.1.parentSpanId" equals the stored value "parent_span_id" And the trace payload field "resourceSpans.0.scopeSpans.0.spans.1" string attribute "bugsnag.span.category" equals "resource_load" And the trace payload field "resourceSpans.0.scopeSpans.0.spans.1" double attribute "bugsnag.sampling.p" equals 0.999999 From 906d8c63d81e8e18d21f22f2657ed49b8abd3d56 Mon Sep 17 00:00:00 2001 From: Dan Skinner Date: Fri, 29 Nov 2024 17:08:31 +0000 Subject: [PATCH 10/10] fix test to reset isSecureContext --- packages/delivery-fetch/tests/delivery.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/delivery-fetch/tests/delivery.test.ts b/packages/delivery-fetch/tests/delivery.test.ts index 50b7d698c..89a37e709 100644 --- a/packages/delivery-fetch/tests/delivery.test.ts +++ b/packages/delivery-fetch/tests/delivery.test.ts @@ -357,6 +357,8 @@ describe('Browser Delivery', () => { state: 'success', samplingProbability: undefined }) + + window.isSecureContext = true }) it('omits the bugsnag integrity header when sendPayloadChecksums is false', async () => {