diff --git a/.github/workflows/fsh-shell_pull-request.yml b/.github/workflows/fsh-shell_pull-request.yml index 7856544..a849ba2 100644 --- a/.github/workflows/fsh-shell_pull-request.yml +++ b/.github/workflows/fsh-shell_pull-request.yml @@ -22,7 +22,8 @@ jobs: missing_files="" for i in "${array[@]}" do - [[ ${FILES_CHANGED[*]} =~ $i ]] && echo "$i file found in the changeset" || missing_files="$missing_files $i" + i_lower=$(echo "$i" | tr '[:upper:]' '[:lower:]') + [[ ${FILES_CHANGED[*]} =~ $i_lower ]] && echo "$i file found in the changeset" || missing_files="$missing_files $i" done echo "" [ -z "$missing_files" ] && echo "All mandatory files have been modified" || echo "Following files $missing_files have not been modified" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b866b0..dd3d9ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.24.0] - 2026-03-23 + +### Added + +- Add `trace` tracking to SDK messages for debugging and analytics +- Added `TraceEntry` interface for the tracing information model + ## [1.23.0] - 2026-03-13 ### Added diff --git a/package-lock.json b/package-lock.json index daa400f..966e9b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fsm-shell", - "version": "1.23.0", + "version": "1.24.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fsm-shell", - "version": "1.23.0", + "version": "1.24.0", "license": "Apache-2.0", "devDependencies": { "@types/jasmine": "^3.5.12", diff --git a/package.json b/package.json index 87038dc..8b6bc93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fsm-shell", - "version": "1.23.0", + "version": "1.24.0", "description": "client library for FSM shell", "main": "release/fsm-shell-client.js", "module": "release/fsm-shell-client.es.js", diff --git a/src/Debugger.ts b/src/Debugger.ts index 39e063a..bac6cf4 100644 --- a/src/Debugger.ts +++ b/src/Debugger.ts @@ -1,7 +1,7 @@ - import { EventType, ALL_SHELL_EVENTS_ARRAY } from './ShellEvents'; import { EventDirection, DebugEvent } from './models/debug/debug-event'; import { MessageLogger } from './MessageLogger'; +import { TraceEntry } from './models/trace/trace-entry.model'; interface DebuggableWindow extends Window { fsmShellMessageLogger: MessageLogger | undefined; @@ -10,29 +10,35 @@ interface DebuggableWindow extends Window { interface Routing { to?: string[]; from?: string[]; + trace?: TraceEntry[]; } const FSM_SHELL_DEBUG_KEY = 'cs.fsm-shell.debug'; export class Debugger { - private debugMode: boolean = false; - constructor( - private winRef: Window, - private debugId: string - ) { + constructor(private winRef: Window, private debugId: string) { if (this.debugId) { const win = this.winRef as DebuggableWindow; const localStorageValue = win.localStorage.getItem(FSM_SHELL_DEBUG_KEY); - if (!!localStorageValue && localStorageValue.split(',').some(it => it === debugId)) { + if ( + !!localStorageValue && + localStorageValue.split(',').some((it) => it === debugId) + ) { this.debugMode = true; } } } - public traceEvent(direction: EventDirection, type: EventType, payload: any, routing: Routing, hasHandler: boolean) { - if (this.debugMode && ALL_SHELL_EVENTS_ARRAY.some(it => it === type)) { + public traceEvent( + direction: EventDirection, + type: EventType, + payload: any, + routing: Routing, + hasHandler: boolean + ) { + if (this.debugMode && ALL_SHELL_EVENTS_ARRAY.some((it) => it === type)) { const debugEvent: DebugEvent = { timestamp: new Date(), component: this.debugId, @@ -41,8 +47,9 @@ export class Debugger { handled: direction === 'incoming' ? (hasHandler ? 'yes' : 'no') : 'n/a', to: routing.to, from: routing.from, - payload - } + trace: routing.trace, + payload, + }; this.logEvent(debugEvent); } } @@ -54,5 +61,4 @@ export class Debugger { } win.fsmShellMessageLogger.push(debugEvent, this.debugId); } - } diff --git a/src/ShellSdk.spec.ts b/src/ShellSdk.spec.ts index 628b477..53538e0 100644 --- a/src/ShellSdk.spec.ts +++ b/src/ShellSdk.spec.ts @@ -102,8 +102,8 @@ describe('Shell Sdk', () => { const MOCK_IFRAME: any = { src: ORIGIN1, contentWindow: ORIGIN1, - extensionAssignmentId: outletExtensionAssignmentId - } + extensionAssignmentId: outletExtensionAssignmentId, + }; sdk.registerOutlet(MOCK_IFRAME, outletName); windowMockCallback({ @@ -114,7 +114,7 @@ describe('Shell Sdk', () => { }, }, source: ORIGIN1, - origin: ORIGIN1 + origin: ORIGIN1, }); const arg1 = sdkTarget.postMessage.getCall(0).args[0]; @@ -124,10 +124,289 @@ describe('Shell Sdk', () => { expect(arg1.type).toBe(SHELL_EVENTS.Version1.REQUIRE_CONTEXT); expect(arg1.value.message).toBe('test data'); expect(arg1.value.targetOutletName).toBe(outletName); - expect(arg1.value.targetExtensionAssignmentId).toBe(outletExtensionAssignmentId); + expect(arg1.value.targetExtensionAssignmentId).toBe( + outletExtensionAssignmentId + ); expect(arg2).toBe(sdkOrigin); }); + it('should include trace when forwarding messages from outlet (old SDK)', () => { + sdk = ShellSdk.init(sdkTarget, sdkOrigin, windowMock); + + const outletName = 'my-outlet'; + const extensionAssignmentId = 'ext-123'; + const iframeSrc = ORIGIN1 + '/app'; + const MOCK_IFRAME: any = { + src: iframeSrc, + contentWindow: 'mock-window', + extensionAssignmentId, + }; + sdk.registerOutlet(MOCK_IFRAME, outletName); + + // Old SDK: message without trace + windowMockCallback({ + data: { + type: SHELL_EVENTS.Version1.GET_PERMISSIONS, + value: { objectName: 'test' }, + }, + source: 'mock-window', + origin: ORIGIN1, + }); + + const arg1 = sdkTarget.postMessage.getCall(0).args[0]; + + // trace has 2 entries: outlet entry (created by forwarder) + forwarder's own entry + expect(arg1.trace).toBeDefined(); + expect(arg1.trace.length).toBe(2); + // First entry is created for the outlet (old SDK didn't send trace) + expect(arg1.trace[0].uuid).toBeDefined(); + expect(arg1.trace[0].outletName).toBe(outletName); + expect(arg1.trace[0].extensionAssignmentId).toBe(extensionAssignmentId); + expect(arg1.trace[0].iframeSrc).toBe(iframeSrc); + // Second entry is the forwarder itself (isModal undefined until REQUIRE_CONTEXT received) + expect(arg1.trace[1].initHref).toBeDefined(); + expect(arg1.trace[1].locationHref).toBeDefined(); + expect(arg1.trace[1].isModal).toBeUndefined(); + }); + + it('should include trace when forwarding messages from outlet (new SDK)', () => { + sdk = ShellSdk.init(sdkTarget, sdkOrigin, windowMock); + + const outletName = 'my-outlet'; + const extensionAssignmentId = 'ext-123'; + const iframeSrc = ORIGIN1 + '/app'; + const MOCK_IFRAME: any = { + src: iframeSrc, + contentWindow: 'mock-window', + extensionAssignmentId, + }; + sdk.registerOutlet(MOCK_IFRAME, outletName); + + // New SDK: message with trace (child's self-reported data, missing iframeSrc) + const incomingTrace = [ + { + initHref: 'https://child.example.com/init', + locationHref: 'https://child.example.com/current', + isModal: false, + }, + ]; + + windowMockCallback({ + data: { + type: SHELL_EVENTS.Version1.GET_PERMISSIONS, + value: { objectName: 'test' }, + trace: incomingTrace, + }, + source: 'mock-window', + origin: ORIGIN1, + }); + + const arg1 = sdkTarget.postMessage.getCall(0).args[0]; + + // trace has 2 entries: enriched child entry + forwarder's own entry + expect(arg1.trace).toBeDefined(); + expect(arg1.trace.length).toBe(2); + // First entry is child's entry, enriched by parent with iframeSrc, uuid, etc. + expect(arg1.trace[0].initHref).toBe('https://child.example.com/init'); + expect(arg1.trace[0].locationHref).toBe( + 'https://child.example.com/current' + ); + expect(arg1.trace[0].isModal).toBe(false); // Preserved from child + expect(arg1.trace[0].iframeSrc).toBe(iframeSrc); // Parent added this + expect(arg1.trace[0].uuid).toBeDefined(); // Parent added this + expect(arg1.trace[0].outletName).toBe(outletName); // Parent added this + expect(arg1.trace[0].extensionAssignmentId).toBe(extensionAssignmentId); // Parent added this + // Second entry is the forwarder itself (isModal undefined until REQUIRE_CONTEXT received) + expect(arg1.trace[1].initHref).toBeDefined(); + expect(arg1.trace[1].locationHref).toBeDefined(); + expect(arg1.trace[1].isModal).toBeUndefined(); + }); + + it('should handle undefined outlet properties in trace', () => { + sdk = ShellSdk.init(sdkTarget, sdkOrigin, windowMock); + + const MOCK_IFRAME: any = { + src: ORIGIN1, + contentWindow: ORIGIN1, + // No extensionAssignmentId + }; + sdk.registerOutlet(MOCK_IFRAME, undefined); // No outlet name + + windowMockCallback({ + data: { + type: SHELL_EVENTS.Version1.GET_PERMISSIONS, + value: { objectName: 'test' }, + }, + source: ORIGIN1, + origin: ORIGIN1, + }); + + const arg1 = sdkTarget.postMessage.getCall(0).args[0]; + + // trace has 2 entries: outlet entry + forwarder's own entry + expect(arg1.trace).toBeDefined(); + expect(arg1.trace.length).toBe(2); + // First entry is the outlet (with undefined optional properties) + expect(arg1.trace[0].outletName).toBeUndefined(); + expect(arg1.trace[0].extensionAssignmentId).toBeUndefined(); + expect(arg1.trace[0].iframeSrc).toBe(ORIGIN1); + expect(arg1.trace[0].uuid).toBeDefined(); + // Second entry is the forwarder itself (isModal undefined until REQUIRE_CONTEXT received) + expect(arg1.trace[1].initHref).toBeDefined(); + expect(arg1.trace[1].locationHref).toBeDefined(); + expect(arg1.trace[1].isModal).toBeUndefined(); + }); + + it('should accumulate trace through multiple hops', () => { + sdk = ShellSdk.init(sdkTarget, sdkOrigin, windowMock); + + const outletName = 'middle-outlet'; + const MOCK_IFRAME: any = { + src: ORIGIN1, + contentWindow: ORIGIN1, + extensionAssignmentId: 'ext-middle', + }; + sdk.registerOutlet(MOCK_IFRAME, outletName); + + // Simulate message from nested outlet that already has 2 trace entries + // (original sender + first forwarder) + const incomingTrace = [ + { + uuid: 'nested-uuid', + outletName: 'nested-outlet', + extensionAssignmentId: 'ext-nested', + iframeSrc: 'https://nested.example.com/app', + initHref: 'https://nested.example.com/init', + locationHref: 'https://nested.example.com/current', + isModal: false, + }, + { + initHref: 'https://first-forwarder.example.com/init', + locationHref: 'https://first-forwarder.example.com/current', + isModal: false, + }, + ]; + + windowMockCallback({ + data: { + type: SHELL_EVENTS.Version1.GET_PERMISSIONS, + value: { objectName: 'test' }, + trace: incomingTrace, + }, + source: ORIGIN1, + origin: ORIGIN1, + }); + + const arg1 = sdkTarget.postMessage.getCall(0).args[0]; + + // Should have 3 entries: 2 incoming + this forwarder's entry + expect(arg1.trace).toBeDefined(); + expect(arg1.trace.length).toBe(3); + + // First entry preserved from original sender + expect(arg1.trace[0].outletName).toBe('nested-outlet'); + expect(arg1.trace[0].iframeSrc).toBe('https://nested.example.com/app'); + expect(arg1.trace[0].isModal).toBe(false); + + // Second entry (first forwarder) enriched by this forwarder + expect(arg1.trace[1].initHref).toBe( + 'https://first-forwarder.example.com/init' + ); + expect(arg1.trace[1].iframeSrc).toBe(ORIGIN1); // This forwarder added iframeSrc + expect(arg1.trace[1].isModal).toBe(false); // Preserved from incoming + + // Third entry is this forwarder's own entry (isModal undefined until REQUIRE_CONTEXT received) + expect(arg1.trace[2].initHref).toBeDefined(); + expect(arg1.trace[2].locationHref).toBeDefined(); + expect(arg1.trace[2].isModal).toBeUndefined(); + }); + + it('should build trace at root level (old SDK fallback)', (done) => { + // Initialize as root (shell-host) - no registered outlets like in production + sdk = ShellSdk.init(null as any as Window, sdkOrigin, windowMock); + + const mockSenderWindow = { postMessage: () => {} }; + + sdk.on( + SHELL_EVENTS.Version1.GET_PERMISSIONS, + (value, origin, from, event, trace) => { + // trace has 2 entries: old SDK fallback + root's own entry + expect(trace).toBeDefined(); + expect(trace.length).toBe(2); + // First entry is fallback for old SDK (uses event.origin since cross-origin) + expect(trace[0].locationHref).toBe('https://sender.example.com'); + // Second entry is root's own entry + expect(trace[1].initHref).toBeDefined(); + expect(trace[1].locationHref).toBeDefined(); + done(); + } + ); + + // Message from old SDK (no trace) + windowMockCallback({ + data: { + type: SHELL_EVENTS.Version1.GET_PERMISSIONS, + value: { objectName: 'ServiceCall' }, + }, + source: mockSenderWindow, + origin: 'https://sender.example.com', + }); + }); + + it('should build trace at root level (new SDK with trace)', (done) => { + // Initialize as root (shell-host) + sdk = ShellSdk.init(null as any as Window, sdkOrigin, windowMock); + + const mockSenderWindow = { postMessage: () => {} }; + + // Incoming trace from sender through forwarders + const incomingTrace = [ + { + uuid: 'sender-uuid', + outletName: 'sender-outlet', + iframeSrc: 'https://sender.example.com/app', + initHref: 'https://sender.example.com/init', + locationHref: 'https://sender.example.com/current', + isModal: false, + }, + { + iframeSrc: 'https://forwarder.example.com/app', + initHref: 'https://forwarder.example.com/init', + locationHref: 'https://forwarder.example.com/current', + isModal: false, + }, + ]; + + sdk.on( + SHELL_EVENTS.Version1.GET_PERMISSIONS, + (_value, _origin, _from, _event, trace) => { + // trace has 3 entries: 2 incoming + root's own entry + expect(trace).toBeDefined(); + expect(trace.length).toBe(3); + // First two entries preserved from incoming + expect(trace[0].outletName).toBe('sender-outlet'); + expect(trace[0].isModal).toBe(false); + expect(trace[1].iframeSrc).toBe('https://forwarder.example.com/app'); + expect(trace[1].isModal).toBe(false); + // Third entry is root's own entry (root has no isModal) + expect(trace[2].initHref).toBeDefined(); + expect(trace[2].locationHref).toBeDefined(); + done(); + } + ); + + // Message from new SDK (has trace) + windowMockCallback({ + data: { + type: SHELL_EVENTS.Version1.GET_PERMISSIONS, + value: { objectName: 'ServiceCall' }, + trace: incomingTrace, + }, + source: mockSenderWindow, + origin: 'https://forwarder.example.com', + }); + }); + it('should call multiple subscribers on message event', () => { let handler1Called: boolean = false; let handler2Called: boolean = false; diff --git a/src/ShellSdk.ts b/src/ShellSdk.ts index ebf2740..444bc2d 100644 --- a/src/ShellSdk.ts +++ b/src/ShellSdk.ts @@ -2,6 +2,7 @@ import { EventType, ErrorType, SHELL_EVENTS } from './ShellEvents'; import { SHELL_VERSION_INFO } from './ShellVersionInfo'; import { Debugger } from './Debugger'; import { Outlet } from './models/outlets/outlet.model'; +import { TraceEntry } from './models/trace/trace-entry.model'; import { PayloadValidator } from './validation/interfaces/payload-validator'; import { getEventValidationConfiguration, @@ -23,13 +24,28 @@ function uuidv4() { const DEFAULT_MAXIMUM_DEPTH = 1; +/** + * Safely get location.href from a window. + * Returns undefined for cross-origin windows (security restriction). + */ +function getLocationHref(win: Window | null | undefined): string | undefined { + if (!win) { + return undefined; + } + try { + return win.location.href; + } catch { + return undefined; + } +} + export class ShellSdk { public static VERSION = SHELL_VERSION_INFO.VERSION; public static BUILD_TS = SHELL_VERSION_INFO.BUILD_TS; private static _instance: ShellSdk; private isRoot: boolean; // Is root if on `init`, target value is null. - private isInsideModal: boolean; + private isInsideModal: boolean | undefined; // undefined until first REQUIRE_CONTEXT response, then true/false based on context private validator: null | PayloadValidator = null; private validationMode: ValidationMode = 'client'; private eventValidationConfiguration: EventValidationConfiguration; @@ -48,6 +64,7 @@ export class ShellSdk { private allowedOrigins: string[] = []; private ignoredOrigins: string[] = []; + private selfUrl: string = ''; // Captured at init for trace private constructor( private target: Window, @@ -62,8 +79,14 @@ export class ShellSdk { this.initMessageApi(); this.debugger = new Debugger(winRef, debugId); this.isRoot = target == null; - this.isInsideModal = false; this.eventValidationConfiguration = getEventValidationConfiguration(); + + // Capture self URL at init time for trace + try { + this.selfUrl = window.location.href; + } catch { + // May not be accessible in some environments + } } public static init( @@ -101,7 +124,7 @@ export class ShellSdk { } public isInsideShellModal(): boolean { - return this.isInsideModal; + return !!this.isInsideModal; } public setAllowedOrigins(allowedOrigins: string[] | '*' = []) { @@ -314,8 +337,19 @@ export class ShellSdk { throw new Error("ShellSdk wasn't initialized, origin is missing."); } - this.debugger.traceEvent('outgoing', type, value, { to }, true); - this.target.postMessage({ type, value, to }, this.origin); + // Non-root: start trace chain with self-reported data + const trace: TraceEntry[] | undefined = this.isRoot + ? undefined + : [ + { + initHref: this.selfUrl, + locationHref: getLocationHref(window), + isModal: this.isInsideModal, + }, + ]; + + this.debugger.traceEvent('outgoing', type, value, { to, trace }, true); + this.target.postMessage({ type, value, to, trace }, this.origin); }; this.winRef.addEventListener('message', this.onMessage); } @@ -366,6 +400,7 @@ export class ShellSdk { value: any; from?: string[]; to?: string[]; + trace?: TraceEntry[]; }; // we ignore LOADING SUCCESS as it is handled by the outlet component itself @@ -445,22 +480,75 @@ export class ShellSdk { outlet.name !== undefined ) { payload.value.targetOutletName = outlet.name; - const extensionAssignmentId = (iFrameElement as any).extensionAssignmentId; - if(extensionAssignmentId){ - payload.value.targetExtensionAssignmentId = extensionAssignmentId; + const extensionAssignmentId = (iFrameElement as any) + .extensionAssignmentId; + if (extensionAssignmentId) { + payload.value.targetExtensionAssignmentId = + extensionAssignmentId; } } - // this is the uuid outlet used for routing of source object from = [...from, outlet.uuid]; + + /* + * trace accumulates at each hop (like `from` array). + * - Old SDK: create full entry with all data we can capture + * - New SDK: enrich child's last entry with parent-only data (iframeSrc, uuid, etc.) + * Then add this forwarder's own entry for the next parent to enrich. + */ + const forwardingTrace: TraceEntry[] = payload.trace || []; + + if (forwardingTrace.length === 0) { + // Old SDK: child didn't send trace, create entry with outlet data + forwardingTrace.push({ + uuid: outlet.uuid, + outletName: outlet.name, + extensionAssignmentId: (iFrameElement as any) + .extensionAssignmentId, + iframeSrc: iFrameElement.src, + locationHref: getLocationHref(iFrameElement.contentWindow), + }); + } else { + // New SDK: enrich child's entry with data only parent knows + const lastEntry = forwardingTrace[forwardingTrace.length - 1]; + if (lastEntry) { + if (!lastEntry.iframeSrc) { + lastEntry.iframeSrc = iFrameElement.src; + } + if (!lastEntry.uuid) { + lastEntry.uuid = outlet.uuid; + } + if (!lastEntry.outletName && outlet.name) { + lastEntry.outletName = outlet.name; + } + if (!lastEntry.extensionAssignmentId) { + lastEntry.extensionAssignmentId = ( + iFrameElement as any + ).extensionAssignmentId; + } + } + } + + // Add this forwarder's entry (parent will enrich with iframeSrc, uuid, etc.) + forwardingTrace.push({ + initHref: this.selfUrl, + locationHref: getLocationHref(window), + isModal: this.isInsideModal, + }); + this.debugger.traceEvent( 'outgoing', payload.type, payload.value, - { from }, + { from, trace: forwardingTrace }, true ); this.target.postMessage( - { type: payload.type, value: payload.value, from }, + { + type: payload.type, + value: payload.value, + from, + trace: forwardingTrace, + }, this.origin ); } @@ -559,11 +647,36 @@ export class ShellSdk { // If isRoot or message is for me, we send to subscribers/handlers const subscribers = this.subscribersMap.get(payload.type); + + /* + * Root: finalize trace chain. + * - Old SDK: create fallback entry using event.origin (limited info) + * - Always: add root's own entry to complete the chain + */ + const trace: TraceEntry[] = payload.trace ? [...payload.trace] : []; + + if (this.isRoot) { + const source: Window = event.source as Window; + + if (source && trace.length === 0) { + // Old SDK fallback: capture origin (full URL if same-origin, origin only if cross-origin) + trace.push({ + locationHref: getLocationHref(source) || event.origin, + }); + } + + // Complete the chain with root's entry + trace.push({ + initHref: this.selfUrl || undefined, + locationHref: getLocationHref(window), + }); + } + this.debugger.traceEvent( 'incoming', payload.type, payload.value, - { from: payload.from }, + { from: payload.from, trace }, !!subscribers ); @@ -587,7 +700,8 @@ export class ShellSdk { payload.type === SHELL_EVENTS.Version1.SET_VIEW_STATE ? null : payload.from, - event + event, + trace ); } } diff --git a/src/index.ts b/src/index.ts index 091a35b..a6c91b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,7 +50,8 @@ import { SetTitleRequest, SetViewStateRequest, SetViewStateResponse, - UiPermissions + TraceEntry, + UiPermissions, } from './models/index'; export { @@ -85,5 +86,6 @@ export { SetTitleRequest, SetViewStateRequest, SetViewStateResponse, - UiPermissions + TraceEntry, + UiPermissions, }; diff --git a/src/models/debug/debug-event.ts b/src/models/debug/debug-event.ts index 5db2f58..1b42345 100644 --- a/src/models/debug/debug-event.ts +++ b/src/models/debug/debug-event.ts @@ -1,4 +1,5 @@ import { EventType } from '../../ShellEvents'; +import { TraceEntry } from '../trace/trace-entry.model'; export type EventDirection = 'incoming' | 'outgoing' | 'blocked'; export type EventHandledLabel = 'yes' | 'no' | 'n/a'; @@ -11,5 +12,6 @@ export interface DebugEvent { type: EventType; to?: string[]; from?: string[]; + trace?: TraceEntry[]; payload: T; } diff --git a/src/models/index.ts b/src/models/index.ts index 8f7797f..b6c4fbb 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -48,3 +48,5 @@ export { AuthResponse } from './authentication/auth-response.model'; export { RequireContextRequest } from './require-context/require-context-request.model'; export { Outlet } from './outlets/outlet.model'; + +export { TraceEntry } from './trace/trace-entry.model'; diff --git a/src/models/trace/trace-entry.model.ts b/src/models/trace/trace-entry.model.ts new file mode 100644 index 0000000..df691a5 --- /dev/null +++ b/src/models/trace/trace-entry.model.ts @@ -0,0 +1,17 @@ +/** + * Represents one hop in the message chain from source to shell-host. + * Each entry is added/enriched as the message traverses iframe layers. + * + * Data is captured from two sources: + * - Self-reported: initHref, locationHref, isModal (set by the sender) + * - Parent-enriched: uuid, outletName, extensionAssignmentId, iframeSrc (set by the parent who owns the iframe) + */ +export interface TraceEntry { + uuid?: string; // Routing UUID assigned by parent at registerOutlet() + outletName?: string; // Human-readable name from registerOutlet(iframe, name) + extensionAssignmentId?: string; // SAP extension assignment ID (if set on iframe element) + iframeSrc?: string; // iframe.src attribute (set by parent, child can't access its own) + initHref?: string; // URL when ShellSdk.init() was called (self-reported) + locationHref?: string; // URL at emit/forward time (self-reported, may differ after SPA navigation) + isModal?: boolean; // True if sender is inside a shell modal (self-reported) +}