@@ -180,12 +180,12 @@ export type IframeSandboxSettingsConfig = Pick<
180
180
'processSignal' | 'edgeFnFetchClient' | 'edgeFnDownloadURL'
181
181
>
182
182
183
- const consoleWarnProcessSignal = ( ) =>
184
- console . warn (
185
- 'processSignal is not defined - have you set up auto-instrumentation on app.segment.com?'
186
- )
183
+ const PROCESS_SIGNAL_UNDEFINED =
184
+ 'processSignal is not defined - have you set up auto-instrumentation on app.segment.com?'
187
185
188
- export class IframeSandboxSettings {
186
+ const consoleWarnProcessSignal = ( ) => console . warn ( PROCESS_SIGNAL_UNDEFINED )
187
+
188
+ export class IframeWorkerSandboxSettings {
189
189
/**
190
190
* Should look like:
191
191
* ```js
@@ -227,10 +227,10 @@ export interface SignalSandbox {
227
227
}
228
228
229
229
export class WorkerSandbox implements SignalSandbox {
230
- settings : IframeSandboxSettings
230
+ settings : IframeWorkerSandboxSettings
231
231
jsSandbox : CodeSandbox
232
232
233
- constructor ( settings : IframeSandboxSettings ) {
233
+ constructor ( settings : IframeWorkerSandboxSettings ) {
234
234
this . settings = settings
235
235
this . jsSandbox = new JavascriptSandbox ( )
236
236
}
@@ -343,3 +343,186 @@ export class NoopSandbox implements SignalSandbox {
343
343
}
344
344
destroy ( ) : void { }
345
345
}
346
+
347
+ /**
348
+ * window.addEventListener('message', async (event) => {
349
+ const { type, payload } = event.data
350
+ if (type === 'execute') {
351
+ try {
352
+ const { signal, signals, analytics, SignalType, EventType, NavigationAction } = payload
353
+ await processSignal(signal, { analytics, signals, SignalType, EventType, NavigationAction })
354
+ event.source.postMessage({ type: 'result', payload: analytics.getCalls() }, '*')
355
+ } catch (err) {
356
+ event.source.postMessage({ type: 'error', error: err.message }, '*')
357
+ }
358
+ }
359
+ })
360
+
361
+ window.parent.postMessage('iframe_ready', '*')
362
+ */
363
+
364
+ const noramizeMethodCallsWithArgResolver = (
365
+ methodCalls : AnalyticsMethodCalls
366
+ ) => {
367
+ const normalizedRuntime = new AnalyticsRuntime ( )
368
+ Object . entries ( methodCalls ) . forEach ( ( [ methodName , calls ] ) => {
369
+ calls . forEach ( ( args ) => {
370
+ // @ts -ignore
371
+ normalizedRuntime [ methodName ] ( ...args )
372
+ } )
373
+ } )
374
+ return normalizedRuntime . getCalls ( )
375
+ }
376
+ export class IframeSandbox implements SignalSandbox {
377
+ private iframe : HTMLIFrameElement
378
+ private iframeReady : Promise < void >
379
+ private _resolveReady ! : ( ) => void
380
+ edgeFnUrl : string
381
+
382
+ constructor ( edgeFnUrl : string ) {
383
+ this . edgeFnUrl = edgeFnUrl
384
+ this . iframe = document . createElement ( 'iframe' )
385
+ this . iframe . id = 'segment-signals-sandbox'
386
+ this . iframe . style . display = 'none'
387
+ this . iframe . src = 'about:blank'
388
+ document . body . appendChild ( this . iframe )
389
+ this . iframeReady = new Promise ( ( res ) => {
390
+ this . _resolveReady = res
391
+ } )
392
+
393
+ void window . addEventListener ( 'message' , ( e ) => {
394
+ if ( e . source === this . iframe . contentWindow && e . data === 'iframe_ready' ) {
395
+ this . iframe . contentWindow ! . postMessage (
396
+ {
397
+ type : 'init' ,
398
+ } ,
399
+ '*'
400
+ )
401
+ this . _resolveReady ( )
402
+ }
403
+ } )
404
+
405
+ const doc = this . iframe . contentDocument !
406
+ doc . open ( )
407
+ doc . write (
408
+ `<!DOCTYPE html><html><head><script id="edge-fn" src=${ this . edgeFnUrl } ></script></head><body></body></html>`
409
+ )
410
+ doc . close ( )
411
+
412
+ // External signal processor script
413
+ // Inject runtime via Blob (CSP-safe)
414
+ const runtimeJs = `
415
+ const signalsScript = document.getElementById('edge-fn')
416
+ signalsScript.onload = () => {
417
+ window.parent.postMessage('iframe_ready', '*')
418
+ }
419
+
420
+ class AnalyticsRuntimeProxy {
421
+ constructor() {
422
+ this.calls = new Map();
423
+ }
424
+ getFormattedCalls() {
425
+ return Object.fromEntries(this.calls); // call in {track: [args]} format
426
+ }
427
+ createProxy() {
428
+ return new Proxy({}, {
429
+ get: (_, methodName) => {
430
+ return (...args) => {
431
+ if (!this.calls.has(methodName)) {
432
+ this.calls.set(methodName, []);
433
+ }
434
+ this.calls.get(methodName).push(args);
435
+ };
436
+ },
437
+ });
438
+ }
439
+ }
440
+
441
+
442
+ // expose the signals global
443
+ ${ getRuntimeCode ( ) }
444
+
445
+ window.addEventListener('message', async (event) => {
446
+ const { type, payload } = event.data;
447
+
448
+
449
+ if (type === 'execute') {
450
+ try {
451
+ const analyticsProxy = new AnalyticsRuntimeProxy();
452
+ window.analytics = analyticsProxy.createProxy();
453
+ if (!payload.signal) {
454
+ throw new Error('invariant: no signal found')
455
+ }
456
+ if (!payload.signalBuffer) {
457
+ throw new Error('invariant: no signalBuffer found')
458
+ }
459
+ if (!payload.constants) {
460
+ throw new Error('invariant: no constants found')
461
+ }
462
+ if (typeof processSignal === 'undefined') {
463
+ throw new Error('processSignal is undefined')
464
+ }
465
+
466
+ const signalBuffer = payload.signalBuffer
467
+ const signal = payload.signal
468
+ const constants = payload.constants
469
+ Object.entries(constants).forEach(([key, value]) => { // expose constants as globals
470
+ window[key] = value;
471
+ });
472
+ window.signals.signalBuffer = signalBuffer; // signals is exposed as part of get runtimeCode
473
+ window.processSignal(signal, { signals, constants })
474
+ event.source.postMessage({ type: 'execution_result', payload: analyticsProxy.getFormattedCalls() }, '*');
475
+ } catch(err) {
476
+ event.source.postMessage({ type: 'execution_error', error: err }, '*');
477
+ }
478
+ }
479
+ });
480
+
481
+
482
+ `
483
+ const blob = new Blob ( [ runtimeJs ] , { type : 'application/javascript' } )
484
+ const runtimeScript = doc . createElement ( 'script' )
485
+ runtimeScript . src = URL . createObjectURL ( blob )
486
+
487
+ doc . head . appendChild ( runtimeScript )
488
+ }
489
+
490
+ async execute (
491
+ signal : Signal ,
492
+ signals : Signal [ ]
493
+ ) : Promise < AnalyticsMethodCalls > {
494
+ await this . iframeReady
495
+
496
+ return new Promise ( ( resolve , reject ) => {
497
+ const handler = ( e : MessageEvent ) => {
498
+ if ( e . source !== this . iframe . contentWindow ) return
499
+ if ( e . data ?. type === 'execution_result' ) {
500
+ window . removeEventListener ( 'message' , handler )
501
+ resolve ( noramizeMethodCallsWithArgResolver ( e . data . payload ) )
502
+ }
503
+ if ( e . data ?. type === 'execution_error' ) {
504
+ window . removeEventListener ( 'message' , handler )
505
+ reject ( e . data . error )
506
+ }
507
+ }
508
+
509
+ window . addEventListener ( 'message' , handler )
510
+
511
+ this . iframe . contentWindow ! . postMessage (
512
+ {
513
+ type : 'execute' ,
514
+ payload : {
515
+ signal,
516
+ signalBuffer : signals ,
517
+ constants : WebRuntimeConstants ,
518
+ } ,
519
+ } ,
520
+ '*'
521
+ )
522
+ } )
523
+ }
524
+
525
+ destroy ( ) {
526
+ this . iframe . remove ( )
527
+ }
528
+ }
0 commit comments