Skip to content

Add signals strategy configuration for customers with restrictive CSPs #1284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 30, 2025
5 changes: 5 additions & 0 deletions .changeset/eight-adults-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@segment/analytics-signals': minor
---

Fix CSP errors with sandboxStrategy: global
5 changes: 5 additions & 0 deletions .changeset/empty-eagles-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@segment/analytics-signals': minor
---

Update max signals in buffer to 100
11 changes: 9 additions & 2 deletions packages/signals/signals-example/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React TypeScript App</title>
<!---

1. Requres 'unsafe-inline'
- Refused to execute inline script because it violates the following Content Security Policy directive: [directive] Either the 'unsafe-inline' keyword, a hash ('sha256-XDT/UwTV/dYYXFad1cmKD+q1zZNK8KGVRYvIdvkmo7I='), or a nonce ('nonce-...') is required to enable inline execution.
2. Requires 'unsafe-eval'
processSignal() error in sandbox Error: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive
-->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://*.segment.com https://*.googletagmanager.com blob:">
</head>

<body>
<div id="root"></div>
</body>

</html>
</html>
2 changes: 1 addition & 1 deletion packages/signals/signals-example/src/lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const isStage = process.env.STAGE === 'true'

const signalsPlugin = new SignalsPlugin({
...(isStage ? { apiHost: 'signals.segment.build/v1' } : {}),
// enableDebugLogging: true,
sandboxStrategy: 'global',
// processSignal: processSignalExample,
})

Expand Down
3 changes: 2 additions & 1 deletion packages/signals/signals-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
"scripts": {
".": "yarn run -T turbo run --filter=@internal/signals-integration-tests...",
"build": "webpack",
"test:int": "playwright test",
"test:int": "playwright test && SKIP_BUILD=true yarn test:global-sandbox",
"test:vanilla": "playwright test src/tests/signals-vanilla",
"test:perf": "playwright test src/tests/performance",
"test:custom": "playwright test src/tests/custom",
"test:global-sandbox": "SANDBOX_STRATEGY=global playwright test src/tests/signals-vanilla src/tests/custom",
"watch": "webpack -w",
"lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'",
"concurrently": "yarn run -T concurrently",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { FullConfig } from '@playwright/test'
import { execSync } from 'child_process'
import { envConfig } from './src/helpers/env-config'

export default function globalSetup(_cfg: FullConfig) {
console.log('Executing global setup...')
execSync('yarn build', { stdio: 'inherit' })
console.log('Finished global setup.')
console.log(`Executing playwright.global-setup.ts...\n`)
console.log(`Using envConfig: ${JSON.stringify(envConfig, undefined, 2)}\n`)
if (process.env.SKIP_BUILD !== 'true') {
console.log(`Executing yarn build:\n`)
execSync('yarn build', { stdio: 'inherit' })
}
console.log('Finished global setup. Should start running tests.\n')
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SignalAPIRequestBuffer,
TrackingAPIRequestBuffer,
} from './network-utils'
import { envConfig } from './env-config'

export class BasePage {
protected page!: Page
Expand Down Expand Up @@ -63,6 +64,7 @@ export class BasePage {
await this.page.goto(url, { waitUntil: 'domcontentloaded' })
if (!options.skipSignalsPluginInit) {
void this.invokeAnalyticsLoad({
sandboxStrategy: envConfig.SANDBOX_STRATEGY,
flushInterval: 500,
...signalSettings,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SignalsPluginSettingsConfig } from '@segment/analytics-signals'

// This is for testing with the global sandbox strategy with an npm script, that executes processSignal in the global scope
// If we change this to be the default, this can be rejiggered
const SANDBOX_STRATEGY = (process.env.SANDBOX_STRATEGY ??
'iframe') as SignalsPluginSettingsConfig['sandboxStrategy']

export const envConfig = {
SANDBOX_STRATEGY,
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { waitForCondition } from '../../helpers/playwright-utils'
import { IndexPage } from './index-page'
import type { SegmentEvent } from '@segment/analytics-next'

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`

test('Collecting signals whenever a user selects an item', async ({ page }) => {
const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'
import { waitForCondition } from '../../helpers/playwright-utils'
import { IndexPage } from './index-page'

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`

test('Collecting signals whenever a user enters text input and focuses out', async ({
page,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ declare global {

const basicEdgeFn = `
// this is a process signal function
const processSignal = (signal) => {
globalThis.processSignal = (signal) => {
if (signal.type === 'interaction') {
const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']'
analytics.track(eventName, signal.data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const snapshot = (
test('Segment events', async ({ page }) => {
const basicEdgeFn = `
// this is a process signal function
const processSignal = (signal) => {
globalThis.processSignal = (signal) => {
if (signal.type === 'interaction' && signal.data.eventType === 'click') {
analytics.identify('john', { found: true })
analytics.group('foo', { hello: 'world' })
Expand Down Expand Up @@ -65,7 +65,7 @@ test('Should dispatch events from signals that occurred before analytics was ins
page,
}) => {
const edgeFn = `
const processSignal = (signal) => {
globalThis.processSignal = (signal) => {
if (signal.type === 'navigation' && signal.data.action === 'pageLoad') {
analytics.page('dispatched from signals - navigation')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IndexPage } from './index-page'

const basicEdgeFn = `
// this is a process signal function
const processSignal = (signal) => {
globalThis.processSignal = (signal) => {
if (signal.type === 'interaction') {
const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']'
analytics.track(eventName, signal.data)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`
let indexPage: IndexPage
test.beforeEach(async ({ page }) => {
indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'
import { waitForCondition } from '../../helpers/playwright-utils'
import { IndexPage } from './index-page'

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`

test('Collecting signals whenever a user enters text input', async ({
page,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`

let indexPage: IndexPage

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`

test('network signals allow and disallow list', async ({ page }) => {
const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'
import { commonSignalData } from '../../helpers/fixtures'
import { IndexPage } from './index-page'

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`

test.describe('network signals - fetch', () => {
let indexPage: IndexPage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`

test.describe('network signals - XHR', () => {
let indexPage: IndexPage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { pTimeout } from '@segment/analytics-core'
* If a signal is generated, the signal buffer should be reset
* when the user clicks on the complex button.
*/
const edgeFn = `const processSignal = (signal) => {
const edgeFn = `globalThis.processSignal = (signal) => {
// create a custom signal to echo out the current signal buffer
if (signal.type === 'userDefined') {
analytics.track('current signal buffer', { signalBuffer: signals.signalBuffer })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const basicEdgeFn = `
const processSignal = (signal) => {
globalThis.processSignal = (signal) => {
// test that constants are properly injected
if (typeof EventType !== 'object') {
throw new Error('EventType is missing?')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IndexPage } from './index-page'
const indexPage = new IndexPage()

test('should find the most recent signal', async ({ page }) => {
const basicEdgeFn = `const processSignal = (signal) => {
const basicEdgeFn = `globalThis.processSignal = (signal) => {
if (signal.type === 'interaction' && signal.data.target.id === 'complex-button') {
const mostRecentSignal = signals.find(signal, 'userDefined')
if (mostRecentSignal.data.num === 2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { waitForCondition } from '../../helpers/playwright-utils'

const indexPage = new IndexPage()

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`

test('debug ingestion disabled and sample rate 0 -> will not send the signal', async ({
page,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'
import { waitForCondition } from '../../helpers/playwright-utils'
import { IndexPage } from './index-page'

const basicEdgeFn = `const processSignal = (signal) => {}`
const basicEdgeFn = `globalThis.processSignal = (signal) => {}`

test('redaction enabled -> will XXX the value of text input', async ({
page,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IndexPage } from './index-page'

const basicEdgeFn = `
// this is a process signal function
const processSignal = (signal) => {
globalThis.processSignal = (signal) => {
if (signal.type === 'interaction') {
analytics.track('hello', { myAnonId: signal.anonymousId, myTimestamp: signal.timestamp })
}
Expand Down
8 changes: 8 additions & 0 deletions packages/signals/signals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ signalsPlugin.addSignal({ someData: 'foo' })
}
```

### Sandbox Strategies
If getting CSP errors, you can use the experimental 'global' sandbox strategy.

```ts
new SignalsPlugin({ sandboxStrategy: 'global' })
```


### Debugging
Debug mode **MUST** be enabled on the client to VIEW signals on segment.com.

Expand Down
2 changes: 1 addition & 1 deletion packages/signals/signals/src/core/buffer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface IDBPObjectStoreSignals
'readonly' | 'readwrite' | 'versionchange'
> {}

const MAX_BUFFER_SIZE_DEFAULT = 50
const MAX_BUFFER_SIZE_DEFAULT = 100

interface StoreSettings {
maxBufferSize?: number
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,55 @@
import { Signal } from '@segment/analytics-signals-runtime'
import { logger } from '../../../lib/logger'
import { SignalBuffer } from '../../buffer'
import { SignalsSubscriber, SignalsMiddlewareContext } from '../../emitter'
import { SignalEventProcessor } from '../../processor/processor'
import { Sandbox, SandboxSettings } from '../../processor/sandbox'
import {
normalizeEdgeFunctionURL,
GlobalScopeSandbox,
WorkerSandbox,
IframeSandboxSettings,
SignalSandbox,
NoopSandbox,
} from '../../processor/sandbox'

export class SignalsEventProcessorSubscriber implements SignalsSubscriber {
processor!: SignalEventProcessor
buffer!: SignalBuffer
load(ctx: SignalsMiddlewareContext) {
this.buffer = ctx.buffer
this.processor = new SignalEventProcessor(
ctx.analyticsInstance,
new Sandbox(new SandboxSettings(ctx.unstableGlobalSettings.sandbox))
const sandboxSettings = ctx.unstableGlobalSettings.sandbox
const normalizedEdgeFunctionURL = normalizeEdgeFunctionURL(

Check warning on line 21 in packages/signals/signals/src/core/middleware/event-processor/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/signals/signals/src/core/middleware/event-processor/index.ts#L20-L21

Added lines #L20 - L21 were not covered by tests
sandboxSettings.functionHost,
sandboxSettings.edgeFnDownloadURL
)

let sandbox: SignalSandbox

if (!normalizedEdgeFunctionURL) {
console.warn(

Check warning on line 29 in packages/signals/signals/src/core/middleware/event-processor/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/signals/signals/src/core/middleware/event-processor/index.ts#L28-L29

Added lines #L28 - L29 were not covered by tests
`No processSignal function found. Have you written a processSignal function on app.segment.com?`
)
logger.debug('Initializing sandbox: noop')
sandbox = new NoopSandbox()
} else if (
sandboxSettings.sandboxStrategy === 'iframe' ||
sandboxSettings.processSignal

Check warning on line 36 in packages/signals/signals/src/core/middleware/event-processor/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/signals/signals/src/core/middleware/event-processor/index.ts#L32-L36

Added lines #L32 - L36 were not covered by tests
) {
logger.debug('Initializing sandbox: iframe')
sandbox = new WorkerSandbox(

Check warning on line 39 in packages/signals/signals/src/core/middleware/event-processor/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/signals/signals/src/core/middleware/event-processor/index.ts#L38-L39

Added lines #L38 - L39 were not covered by tests
new IframeSandboxSettings({
processSignal: sandboxSettings.processSignal,
edgeFnDownloadURL: normalizedEdgeFunctionURL,
})
)
} else {
logger.debug('Initializing sandbox: global scope')
sandbox = new GlobalScopeSandbox({

Check warning on line 47 in packages/signals/signals/src/core/middleware/event-processor/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/signals/signals/src/core/middleware/event-processor/index.ts#L45-L47

Added lines #L45 - L47 were not covered by tests
edgeFnDownloadURL: normalizedEdgeFunctionURL,
})
}

this.processor = new SignalEventProcessor(ctx.analyticsInstance, sandbox)

Check warning on line 52 in packages/signals/signals/src/core/middleware/event-processor/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/signals/signals/src/core/middleware/event-processor/index.ts#L52

Added line #L52 was not covered by tests
}
async process(signal: Signal) {
return this.processor.process(signal, await this.buffer.getAll())
Expand Down
Loading