Skip to content
Merged
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 .github/workflows/semantic-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ jobs:
run: pnpm test
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.COMMERCELAYER_CI_TOKEN }}
run: pnpx semantic-release
47 changes: 47 additions & 0 deletions specs/sdk-telemetry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, test } from 'vitest'
import { application, CommerceLayer, SDK_VERSION } from '../src'
import { handleError, interceptRequest } from '../test/common'

const CLIENT_HEADER = 'X-CL-SDK'
const baseConfig = { organization: 'test-org', accessToken: 'fake-token' } as const

describe('SDK client identification header', () => {
test('sends `js/<version>` by default', async () => {
const client = CommerceLayer(baseConfig)
client.addRequestInterceptor((request) => {
const headers = request.options.headers as Record<string, string>
expect(headers[CLIENT_HEADER]).toBe(`js/${SDK_VERSION}`)
return interceptRequest()
})
await application
.retrieve({})
.catch(handleError)
.finally(() => client.removeInterceptor('request'))
})

test('omits the header when `telemetry: false`', async () => {
const client = CommerceLayer({ ...baseConfig, telemetry: false })
client.addRequestInterceptor((request) => {
const headers = request.options.headers as Record<string, string>
expect(headers[CLIENT_HEADER]).toBeUndefined()
return interceptRequest()
})
await application
.retrieve({})
.catch(handleError)
.finally(() => client.removeInterceptor('request'))
})

test('ignores customer overrides (SDK value wins)', async () => {
const client = CommerceLayer(baseConfig)
client.addRequestInterceptor((request) => {
const headers = request.options.headers as Record<string, string>
expect(headers[CLIENT_HEADER]).toBe(`js/${SDK_VERSION}`)
return interceptRequest()
})
await application
.retrieve({}, { headers: { [CLIENT_HEADER]: 'spoofed/9.9.9' } })
.catch(handleError)
.finally(() => client.removeInterceptor('request'))
})
})
13 changes: 12 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type { Fetch, FetchClientOptions, FetchRequestOptions, FetchResponse } fr
import { fetchURL } from './fetch'
import type { InterceptorManager } from './interceptor'
import { extractTokenData, isTokenExpired } from './util'
import { SDK_VERSION } from './version'

const CLIENT_HEADER_NAME = 'X-CL-SDK'

const debug = Debug('client')

Expand All @@ -24,6 +27,7 @@ type RequestConfig = {
userAgent?: string
fetch?: Fetch
refreshToken?: RefreshToken
telemetry?: boolean
}

type ApiConfig = {
Expand Down Expand Up @@ -83,6 +87,9 @@ class ApiClient {
Authorization: 'Bearer ' + this.#accessToken,
}

// SDK client identification — opt out via { telemetry: false } at init time
if (options.telemetry !== false) headers[CLIENT_HEADER_NAME] = `js/${SDK_VERSION}`

// Set User-Agent
if (options.userAgent) headers['User-Agent'] = options.userAgent

Expand Down Expand Up @@ -226,7 +233,11 @@ class ApiClient {
const customHeaders: RequestHeaders = {}
if (headers) {
for (const [name, value] of Object.entries(headers))
if (!['accept', 'content-type', 'authorization', 'user-agent'].includes(name.toLowerCase()))
if (
!['accept', 'content-type', 'authorization', 'user-agent', CLIENT_HEADER_NAME.toLowerCase()].includes(
name.toLowerCase(),
)
)
customHeaders[name] = value
}
return customHeaders
Expand Down
7 changes: 5 additions & 2 deletions src/commercelayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ import type {
ResponseObj,
} from './interceptor'
import { ApiResourceAdapter, type ResourcesInitConfig } from './resource'
import { SDK_VERSION } from './version'

const debug = Debug('commercelayer')

// Autogenerated version number, do not remove this line
export const API_SCHEMA_VERSION = 'latest'

export { SDK_VERSION }

// SDK local configuration
// biome-ignore lint/complexity/noBannedTypes: left as placeholder for future SDK config options
type SdkConfig = {
// abc?: string
/** Set to `false` to omit the `X-CL-SDK` request header that identifies the SDK and its version. Defaults to `true`. */
telemetry?: boolean
}

type CommerceLayerInitConfig = SdkConfig & ResourcesInitConfig
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Resource adapters
export * from './api'
// SDK
export { CommerceLayer, default } from './commercelayer'
export { CommerceLayer, default, SDK_VERSION } from './commercelayer'

// Commerce Layer static functions
export { CommerceLayerStatic } from './static'
Expand Down
3 changes: 3 additions & 0 deletions src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pkg from '../package.json' with { type: 'json' }

export const SDK_VERSION: string = pkg.version
Loading