Skip to content

Commit 68171d5

Browse files
committed
hacky sandbox strategy
1 parent a347bce commit 68171d5

File tree

3 files changed

+58
-30
lines changed

3 files changed

+58
-30
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class SignalsEventProcessorSubscriber implements SignalsSubscriber {
3030
`No processSignal function found. Have you written a processSignal function on app.segment.com?`
3131
)
3232
sandbox = new NoopSandbox()
33-
} else if (!GLOBAL_SCOPE_SANDBOX) {
33+
} else if (!GLOBAL_SCOPE_SANDBOX || sandboxSettings.processSignal) {
3434
sandbox = new WorkerSandbox(
3535
new WorkerSandboxSettings({
3636
processSignal: sandboxSettings.processSignal,

packages/signals/signals/src/core/processor/processor.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export class SignalEventProcessor {
1212
}
1313

1414
async process(signal: Signal, signals: Signal[]) {
15-
await this.sandbox.isLoaded()
1615
let analyticsMethodCalls: AnalyticsMethodCalls | undefined
1716
try {
1817
analyticsMethodCalls = await this.sandbox.execute(signal, signals)

packages/signals/signals/src/core/processor/sandbox.ts

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { logger } from '../../lib/logger'
22
import { createWorkerBox, WorkerBoxAPI } from '../../lib/workerbox'
33
import { resolvers } from './arg-resolvers'
4-
import { AnalyticsRuntimePublicApi } from '../../types'
4+
import { AnalyticsRuntimePublicApi, ProcessSignal } from '../../types'
55
import { replaceBaseUrl } from '../../lib/replace-base-url'
66
import {
77
Signal,
@@ -172,6 +172,13 @@ export type SandboxSettingsConfig = {
172172
processSignal: string | undefined
173173
edgeFnDownloadURL: string | undefined
174174
edgeFnFetchClient?: typeof fetch
175+
/**
176+
* What sandbox strategy to use
177+
* default - use a web worker and regular evaluation
178+
* globalScope - evaluate everything in the global scope -- this avoids CSP errors
179+
* @default 'default'
180+
*/
181+
sandboxStrategy?: 'default' | 'globalScope'
175182
}
176183

177184
export type WorkerboxSettingsConfig = Pick<
@@ -203,7 +210,6 @@ export class WorkerSandboxSettings {
203210
}
204211

205212
export interface SignalSandbox {
206-
isLoaded(): Promise<void>
207213
execute(
208214
signal: Signal,
209215
signals: Signal[]
@@ -220,10 +226,6 @@ export class WorkerSandbox implements SignalSandbox {
220226
this.jsSandbox = new JavascriptSandbox()
221227
}
222228

223-
isLoaded(): Promise<any> {
224-
return Promise.resolve() // TODO
225-
}
226-
227229
async execute(
228230
signal: Signal,
229231
signals: Signal[]
@@ -252,55 +254,82 @@ export class WorkerSandbox implements SignalSandbox {
252254
}
253255
}
254256

255-
// This is not ideal -- but processSignal currently depends on
256-
257-
const processWithGlobals = (
257+
// ProcessSignal unfortunately uses globals. This should change.
258+
// For now, we are setting up the globals between each invocation
259+
const processWithGlobalScopeExecutionEnv = (
260+
signal: Signal,
258261
signalBuffer: Signal[]
259262
): AnalyticsMethodCalls | undefined => {
260263
const g = globalThis as any
264+
const processSignal: ProcessSignal = g['processSignal']
265+
266+
if (typeof processSignal == 'undefined') {
267+
console.warn('no processSignal function is defined in the global scope')
268+
return undefined
269+
}
270+
261271
// Load all constants into the global scope
262272
Object.entries(WebRuntimeConstants).forEach(([key, value]) => {
263273
g[key] = value
264274
})
265275

266276
// processSignal expects a global called `signals` -- of course, there can local variable naming conflict on the client, which is why globals were a bad idea.
267-
g['signals'] = new WebSignalsRuntime(signalBuffer)
277+
const analytics = new AnalyticsRuntime()
278+
const signals = new WebSignalsRuntime(signalBuffer)
279+
280+
const originalAnalytics = g.analytics
281+
try {
282+
if (g['analytics'] instanceof AnalyticsRuntime) {
283+
console.warn(
284+
'Invariant: analytics variable was not properly restored on the previous execution. This indicates a concurrency bug'
285+
)
268286

269-
// expect analytics to be instantiated -- this will conflict in the global scope TODO
270-
g['analytics'] = new AnalyticsRuntime()
287+
return
288+
}
271289

272-
// another possible namespace conflict?
273-
// @ts-ignore
274-
if (typeof processSignal != 'undefined') {
275-
g['processSignal'](signalBuffer[0])
276-
} else {
277-
console.warn('no processSignal function is defined in the global scope')
290+
g['analytics'] = analytics
291+
292+
g['signals'] = signals
293+
processSignal(signal, {
294+
// we eventually want to get rid of globals and processSignal just uses local variables.
295+
analytics: analytics,
296+
signals: signals,
297+
// constants
298+
EventType: WebRuntimeConstants.EventType,
299+
NavigationAction: WebRuntimeConstants.NavigationAction,
300+
SignalType: WebRuntimeConstants.SignalType,
301+
})
302+
} finally {
303+
// restore globals
304+
g['analytics'] = originalAnalytics
278305
}
279306

280-
return g['analytics'].getCalls()
307+
// this seems like it could potentially cause bugs in async environment
308+
g.analytics = originalAnalytics
309+
return analytics.getCalls()
281310
}
282311

312+
/**
313+
* Sandbox that avoids CSP errors, but evaluates everything globally
314+
*/
283315
interface GlobalScopeSandboxSettings {
284316
edgeFnDownloadURL: string
285317
}
286318
export class GlobalScopeSandbox implements SignalSandbox {
287-
script: Promise<HTMLScriptElement>
288-
async isLoaded(): Promise<void> {
289-
await this.script
290-
}
319+
htmlScriptLoaded: Promise<HTMLScriptElement>
320+
291321
constructor(settings: GlobalScopeSandboxSettings) {
292-
this.script = loadScript(settings.edgeFnDownloadURL)
322+
this.htmlScriptLoaded = loadScript(settings.edgeFnDownloadURL)
293323
}
294324

295-
// eslint-disable-next-line @typescript-eslint/require-await
296-
async execute(_signal: Signal, signals: Signal[]) {
297-
return processWithGlobals(signals)
325+
async execute(signal: Signal, signals: Signal[]) {
326+
await this.htmlScriptLoaded
327+
return processWithGlobalScopeExecutionEnv(signal, signals)
298328
}
299329
destroy(): void {}
300330
}
301331

302332
export class NoopSandbox implements SignalSandbox {
303-
async isLoaded(): Promise<void> {}
304333
execute(_signal: Signal, _signals: Signal[]) {
305334
return Promise.resolve(undefined)
306335
}

0 commit comments

Comments
 (0)