Skip to content

Commit e4fb532

Browse files
authored
Add CSP-specific metrics and better error message (#815)
1 parent a49360a commit e4fb532

File tree

8 files changed

+51
-30
lines changed

8 files changed

+51
-30
lines changed

examples/standalone-playground/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
"hoistingLimits": "workspaces"
66
},
77
"scripts": {
8-
"dev": "yarn concurrently 'yarn run -T watch --filter=standalone-playground' 'sleep 10 && npx http-server .'",
8+
"dev": "yarn concurrently 'yarn run -T watch --filter=standalone-playground' 'sleep 10 && yarn http-server .'",
99
"concurrently": "yarn run -T concurrently"
1010
},
1111
"dependencies": {
1212
"@segment/analytics-next": "workspace:^"
13+
},
14+
"devDependencies": {
15+
"http-server": "14.1.1"
1316
}
1417
}

packages/browser/src/browser/__tests__/csp-detection.test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import jsdom, { JSDOM } from 'jsdom'
22
import unfetch from 'unfetch'
33
import { LegacySettings } from '..'
4-
import { onCSPError } from '../../lib/csp-detection'
54
import { pWhile } from '../../lib/p-while'
65
import { snippet } from '../../tester/__fixtures__/segment-snippet'
76
import * as Factory from '../../test-helpers/factories'
@@ -122,15 +121,18 @@ describe('CSP Detection', () => {
122121
})
123122

124123
it('does not revert to classic when CSP error is report only', async () => {
124+
await import('../standalone')
125125
const ogScripts = Array.from(document.scripts)
126126

127127
const warnSpy = jest.spyOn(console, 'warn')
128-
129-
await onCSPError({
130-
blockedURI: 'cdn.segment.com',
131-
disposition: 'report',
132-
} as unknown as SecurityPolicyViolationEvent)
133-
128+
const cspSpy = jest.fn()
129+
document.addEventListener('securitypolicyviolation', cspSpy)
130+
131+
const event = new window.Event('securitypolicyviolation') as any
132+
event.disposition = 'report'
133+
event.blockedURI = 'cdn.segment.com'
134+
document.dispatchEvent(event)
135+
expect(cspSpy).toBeCalled()
134136
expect(warnSpy).not.toHaveBeenCalled()
135137
expect(Array.from(document.scripts)).toEqual(ogScripts)
136138
})

packages/browser/src/browser/standalone.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,40 @@ import '../lib/csp-detection'
2424
import { shouldPolyfill } from '../lib/browser-polyfill'
2525
import { RemoteMetrics } from '../core/stats/remote-metrics'
2626
import { embeddedWriteKey } from '../lib/embedded-write-key'
27-
import { onCSPError } from '../lib/csp-detection'
27+
import {
28+
loadAjsClassicFallback,
29+
isAnalyticsCSPError,
30+
} from '../lib/csp-detection'
31+
32+
let ajsIdentifiedCSP = false
33+
34+
const sendErrorMetrics = (() => {
35+
const metrics = new RemoteMetrics()
36+
return (tags: string[]) => {
37+
metrics.increment('analytics_js.invoke.error', [
38+
...tags,
39+
`wk:${embeddedWriteKey()}`,
40+
])
41+
}
42+
})()
2843

2944
function onError(err?: unknown) {
3045
console.error('[analytics.js]', 'Failed to load Analytics.js', err)
31-
32-
new RemoteMetrics().increment('analytics_js.invoke.error', [
46+
sendErrorMetrics([
3347
'type:initialization',
3448
...(err instanceof Error
3549
? [`message:${err?.message}`, `name:${err?.name}`]
3650
: []),
37-
`wk:${embeddedWriteKey()}`,
3851
])
3952
}
4053

4154
document.addEventListener('securitypolicyviolation', (e) => {
42-
onCSPError(e).catch(console.error)
55+
if (ajsIdentifiedCSP || !isAnalyticsCSPError(e)) {
56+
return
57+
}
58+
ajsIdentifiedCSP = true
59+
sendErrorMetrics(['type:csp'])
60+
loadAjsClassicFallback().catch(console.error)
4361
})
4462

4563
/**
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const SEGMENT_API_HOST = 'api.segment.io/v1'

packages/browser/src/core/stats/remote-metrics.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { fetch } from '../../lib/fetch'
22
import { version } from '../../generated/version'
33
import { getVersionType } from '../../plugins/segmentio/normalize'
4+
import { SEGMENT_API_HOST } from '../constants'
45

56
export interface MetricsOptions {
67
host?: string
@@ -60,7 +61,7 @@ export class RemoteMetrics {
6061
queue: RemoteMetric[]
6162

6263
constructor(options?: MetricsOptions) {
63-
this.host = options?.host ?? 'api.segment.io/v1'
64+
this.host = options?.host ?? SEGMENT_API_HOST
6465
this.sampleRate = options?.sampleRate ?? 1
6566
this.flushTimer = options?.flushTimer ?? 30 * 1000 /* 30s */
6667
this.maxQueueSize = options?.maxQueueSize ?? 20

packages/browser/src/lib/csp-detection.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
11
import { loadScript } from './load-script'
22
import { getLegacyAJSPath } from './parse-cdn'
33

4-
let ajsIdentifiedCSP = false
5-
6-
export async function onCSPError(
7-
e: SecurityPolicyViolationEvent & { disposition?: 'enforce' | 'report' }
8-
): Promise<void> {
9-
if (e.disposition === 'report') {
10-
return
11-
}
12-
13-
if (!e.blockedURI.includes('cdn.segment') || ajsIdentifiedCSP) {
14-
return
15-
}
16-
17-
ajsIdentifiedCSP = true
4+
type CSPErrorEvent = SecurityPolicyViolationEvent & {
5+
disposition?: 'enforce' | 'report'
6+
}
7+
export const isAnalyticsCSPError = (e: CSPErrorEvent) => {
8+
return e.disposition !== 'report' && e.blockedURI.includes('cdn.segment')
9+
}
1810

11+
export async function loadAjsClassicFallback(): Promise<void> {
1912
console.warn(
20-
'Your CSP policy is missing permissions required in order to run Analytics.js 2.0'
13+
'Your CSP policy is missing permissions required in order to run Analytics.js 2.0',
14+
'https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/upgrade-to-ajs2/#using-a-strict-content-security-policy-on-the-page'
2115
)
2216
console.warn('Reverting to Analytics.js 1.0')
2317

packages/browser/src/plugins/segmentio/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import batch, { BatchingDispatchConfig } from './batched-dispatcher'
1111
import standard, { StandardDispatcherConfig } from './fetch-dispatcher'
1212
import { normalize } from './normalize'
1313
import { scheduleFlush } from './schedule-flush'
14+
import { SEGMENT_API_HOST } from '../../core/constants'
1415

1516
type DeliveryStrategy =
1617
| {
@@ -71,7 +72,7 @@ export function segmentio(
7172
const inflightEvents = new Set<Context>()
7273
const flushing = false
7374

74-
const apiHost = settings?.apiHost ?? 'api.segment.io/v1'
75+
const apiHost = settings?.apiHost ?? SEGMENT_API_HOST
7576
const protocol = settings?.protocol ?? 'https'
7677
const remote = `${protocol}://${apiHost}`
7778

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,7 @@ __metadata:
926926
resolution: "@example/standalone-playground@workspace:examples/standalone-playground"
927927
dependencies:
928928
"@segment/analytics-next": "workspace:^"
929+
http-server: 14.1.1
929930
languageName: unknown
930931
linkType: soft
931932

0 commit comments

Comments
 (0)