Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .buildkite/browser-pipeline.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ steps:
queue: "macos"
timeout_in_minutes: 2
commands:
- BUILD_MODE=CDN buildkite-agent pipeline upload .buildkite/browser-pipeline.yml
- BUILD_MODE=CDN buildkite-agent pipeline upload .buildkite/browser-pipeline.yml
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 20.11.1
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,15 @@ module.exports = {
{
displayName: 'delivery-fetch',
testMatch: ['<rootDir>/packages/delivery-fetch/**/*.test.ts'],
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest/setup/crypto.ts'],
...defaultModuleConfig
},
{
displayName: 'browser',
testMatch: ['<rootDir>/packages/platforms/browser/**/*.test.ts'],
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest/setup/crypto.ts'],
...defaultModuleConfig
},
{
Expand Down
19 changes: 19 additions & 0 deletions jest/setup/crypto.ts
Original file line number Diff line number Diff line change
@@ -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 }
})
9 changes: 8 additions & 1 deletion packages/core/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from './custom-attribute-limits'
import type { Plugin } from './plugin'
import type { Span } from './span'
import { isLogger, isNumber, isObject, isCallbackArray, isPluginArray, isString, isStringArray, isStringWithLength } from './validation'
import { isBoolean, isLogger, isNumber, isObject, isCallbackArray, isPluginArray, isString, isStringArray, isStringWithLength } from './validation'

type SetTraceCorrelation = (traceId: string, spanId: string) => void

Expand Down Expand Up @@ -60,6 +60,7 @@ export interface Configuration {
attributeStringValueLimit?: number
attributeArrayLengthLimit?: number
attributeCountLimit?: number
sendPayloadChecksums?: boolean
}

export interface TestConfiguration {
Expand Down Expand Up @@ -88,6 +89,7 @@ export interface CoreSchema extends Schema {
plugins: ConfigOption<Array<Plugin<Configuration>>>
bugsnag: ConfigOption<BugsnagErrorStatic | undefined>
samplingProbability: ConfigOption<number | undefined>
sendPayloadChecksums: ConfigOption<boolean>
}

export const schema: CoreSchema = {
Expand Down Expand Up @@ -165,6 +167,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
}
}

Expand Down
7 changes: 6 additions & 1 deletion packages/core/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (
start: (config: C | string) => {
const configuration = validateConfig<S, C>(config, options.schema, options.isDevelopment)

// sendPayloadChecksums is false by default unless custom endpoints are not specified
if (typeof config !== 'string' && !config.endpoint) {
configuration.sendPayloadChecksums = ('sendPayloadChecksums' in config && config.sendPayloadChecksums) || true
}
Comment on lines +94 to +97
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not keen on this but not sure of a better way. We need to work out what config options the user passed in to determine the behaviour. const configuration = validateConfig<S, C>(config, options.schema) does more than just validate, it creates a config with all values set based on the schema, using default values if not supplied.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right now this is the place we're performing similar manipulation on the configuration, perhaps we could refactor the validation to include this, possibly separating out validation and 'cleaning' but I think for now this is fine


// 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://<project_api_key>.otlp.bugsnag.com/v1/traces
if (configuration.endpoint === schema.endpoint.defaultValue) {
Expand Down Expand Up @@ -141,7 +146,7 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (

spanFactory.configure(configuration)

const delivery = options.deliveryFactory(configuration.endpoint)
const delivery = options.deliveryFactory(configuration.endpoint, configuration.sendPayloadChecksums)
const probabilityManagerPromise = configuration.samplingProbability === undefined
? ProbabilityManager.create(
options.persistence,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/lib/delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
}
}

Expand Down
11 changes: 6 additions & 5 deletions packages/core/tests/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,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)
})
})

Expand All @@ -358,7 +358,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)
})
})
})
Expand Down Expand Up @@ -530,7 +530,7 @@ describe('Core', () => {
// allow async configuration to complete
await jest.runOnlyPendingTimersAsync()

expect(deliveryFactory).toHaveBeenCalledWith(SECONDARY_ENDPOINT)
expect(deliveryFactory).toHaveBeenCalledWith(SECONDARY_ENDPOINT, false)
})

it('keeps Bugsnag host + apiKey sub-domain for a normal key', async () => {
Expand All @@ -543,7 +543,8 @@ describe('Core', () => {
await jest.runOnlyPendingTimersAsync()

expect(deliveryFactory).toHaveBeenCalledWith(
`https://${BS_KEY}.${PRIMARY_ENDPOINT.slice('https://'.length)}`
`https://${BS_KEY}.${PRIMARY_ENDPOINT.slice('https://'.length)}`,
false
)
})

Expand All @@ -557,7 +558,7 @@ describe('Core', () => {

await jest.runOnlyPendingTimersAsync()

expect(deliveryFactory).toHaveBeenCalledWith(CUSTOM_ENDPOINT)
expect(deliveryFactory).toHaveBeenCalledWith(CUSTOM_ENDPOINT, false)
})
})
})
Expand Down
29 changes: 24 additions & 5 deletions packages/delivery-fetch/lib/delivery.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -40,14 +37,20 @@ 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)

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,
Expand All @@ -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()
}
Loading