import { moduleRuntime } from './core/runtime'; import type { JourneySink } from './core/types'; import type { RequestPolicyConfig } from './core/request-policy'; import { areSharedPoliciesLocked, getSharedSlowRequests, isJourneyEnabled, registerJourneyRuntime, seedSharedPolicies, warnIfSlowRequestsUnset, } from './global'; import { sortEndpointsByLongestMatch, type SlowRequestConfig } from './core/endpoint-policy'; import { instrumentAxios, type AxiosLikeInstance } from './integrations/axios'; import { instrumentFetch, type InstrumentFetchOptions } from './integrations/fetch'; import { exposeAppJourney, instrumentJquery, type JqueryStatic, type WindowWithAppJourney, } from './integrations/jquery'; import { resolveGlobalJquery } from './integrations/jquery/instrument'; import { sendToConsole } from './sinks/console'; import { sendToDatadog } from './sinks/datadog'; /** Optional app-wide / per-bundle configuration. */ export interface JourneyConfig { /** * App-wide slow-request thresholds (shared on `__stJourney` for all package copies). * Package fallback remains `{ defaultMs: 4000, endpoints: [] }` until set. * Desktop / KO hosts should set `defaultMs` and ignore noisy endpoint prefixes. */ slowRequests?: SlowRequestConfig; /** * How attributed aborted/cancelled HTTP counts toward the open journey. * Shared on `__stJourney`. Literal or `(ctx) => …`. Default `'continue'`. */ httpAbortedRequests?: RequestPolicyConfig; /** * How attributed 4xx responses count toward the open journey. * Shared on `__stJourney`. Literal or `(ctx) => …`. Default `'continue'` (no verdict change). */ httpClientErrorRequests?: RequestPolicyConfig; /** * When true (default), untagged instrumented HTTP is attributed to the sole * in-flight step (if exactly one). When false, only explicit `step.stamp()` attributes. * Prefer false on busy desktop / shared jQuery shells with background traffic. * Shared on `__stJourney`. */ autoAttributeRequests?: boolean; /** * Default cap on recorded steps per journey (default 50). Per-journey * `JourneyDef.maxSteps` overrides. Excess steps are dropped and * `tags.steps_truncated` is set. */ maxSteps?: number; /** * Applied when a journey omits `timeoutMs`. * Default is 15m and closes as `excluded` with `journey-idle-timeout`. * Pass `0` to disable this idle timeout. */ defaultJourneyIdleMs?: number; /** * Replace the sink list used when a journey finishes. * Only applied when provided — omit to leave the current list unchanged. * Package default is `[sendToDatadog, sendToConsole]`; pass `[]` to disable emission. * `sendToConsole` is a no-op unless `sessionStorage['st:journey:debug'] === 'true'`. * First `sinks` on this bundle wins; a later `configureJourney({ sinks })` is ignored. */ sinks?: readonly JourneySink[]; /** * Wire this bundle's axios instance (host or MFE). Same as `instrumentAxios(axios)`. * Register any async request interceptors (e.g. auth) before `configureJourney`. * Soft-skips when axios is missing / not axios-like — safe to pass a host instance that may be absent. */ axios?: AxiosLikeInstance | null; /** * Wire fetch. `true` wraps `globalThis.fetch`; pass options for a scoped target / baseURL. * Same as `instrumentFetch(...)`. Soft-skips when the target has no callable `fetch`. * Prefer host-only for `globalThis.fetch` — MFEs inherit the host wrapper. */ fetch?: true | InstrumentFetchOptions | null; /** * Wire jQuery. Pass a `$` instance, or `true` to use `globalThis.$` / `globalThis.jQuery`. * `false` skips wiring. Soft-skips when `$` is missing / not real jQuery. * Prefer host-only for shared `$`. */ jquery?: boolean | JqueryStatic | null; /** * Publish `window.App.Journey` for Knockout / legacy hosts. * `true` uses the real window; pass a target object in tests. */ exposeAppJourney?: boolean | WindowWithAppJourney; } /* * Default emission targets + `__stJourney` bag. Kept as a module side effect so any import * of this file (see index.ts `import './config'`) installs Datadog, console, and * `__stJourney`. configureJourney only replaces sinks when `sinks` is passed explicitly. */ moduleRuntime.setSinks([sendToDatadog, sendToConsole]); registerJourneyRuntime(moduleRuntime); /** * Apply bundle options: request thresholds, sinks, and/or HTTP / legacy wiring. * Optional — call once at startup. Pass this bundle's own axios / `$` / fetch target. * Policy knobs (`slowRequests`, `httpAbortedRequests`, `httpClientErrorRequests`, * `autoAttributeRequests`) are stored on shared `__stJourney` so MFEs inherit host settings. * No-ops when `isJourneyEnabled()` is false (host default or force flag). * Never throws: missing/invalid config and failures soft-skip (warn / error to console). */ export function configureJourney(config: JourneyConfig): void { if (config == null || typeof config !== 'object') { // eslint-disable-next-line no-console -- soft-skip missing host config console.warn('[journey] configureJourney: skipped — missing'); return; } if (!isJourneyEnabled()) { // eslint-disable-next-line no-console -- soft-skip when host default and force are off console.warn( '[journey] configureJourney: skipped — disabled (call setJourneyDefaultEnabled(true) or __stJourney.enable(), then configureJourney again / reload)' ); return; } try { const overlay: { slowRequests?: SlowRequestConfig; httpAbortedRequests?: RequestPolicyConfig; httpClientErrorRequests?: RequestPolicyConfig; autoAttributeRequests?: boolean; } = {}; const policyPatch: Parameters[0] = {}; let hasPolicyKnob = false; if (config.slowRequests !== undefined) { const sorted = { ...config.slowRequests, endpoints: sortEndpointsByLongestMatch(config.slowRequests.endpoints), }; policyPatch.slowRequests = sorted; policyPatch.slowRequestsConfigured = true; overlay.slowRequests = sorted; hasPolicyKnob = true; } if (config.httpAbortedRequests !== undefined) { policyPatch.httpAbortedRequests = config.httpAbortedRequests; overlay.httpAbortedRequests = config.httpAbortedRequests; hasPolicyKnob = true; } if (config.httpClientErrorRequests !== undefined) { policyPatch.httpClientErrorRequests = config.httpClientErrorRequests; overlay.httpClientErrorRequests = config.httpClientErrorRequests; hasPolicyKnob = true; } if (config.autoAttributeRequests !== undefined) { policyPatch.autoAttributeRequests = config.autoAttributeRequests; overlay.autoAttributeRequests = config.autoAttributeRequests; hasPolicyKnob = true; } if (hasPolicyKnob) { if (!areSharedPoliciesLocked()) { seedSharedPolicies(policyPatch); } moduleRuntime.setPolicyOverlay(overlay); } if (config.maxSteps !== undefined) { moduleRuntime.setMaxSteps(config.maxSteps); } if (config.defaultJourneyIdleMs !== undefined) { moduleRuntime.setDefaultJourneyIdleMs(config.defaultJourneyIdleMs); } if (config.sinks !== undefined) { if (!moduleRuntime.trySetConfiguredSinks(config.sinks)) { // eslint-disable-next-line no-console -- first-writer sinks console.warn( '[journey] configureJourney: sinks already set on this runtime — ignoring' ); } } const wiringTransport = config.axios !== undefined || config.fetch !== undefined || (config.jquery !== undefined && config.jquery !== false); if (wiringTransport) { warnIfSlowRequestsUnset(); } if (config.axios !== undefined) { instrumentAxios(config.axios); } if (config.fetch !== undefined) { instrumentFetch(config.fetch === true ? {} : config.fetch); } if (config.jquery === true) { instrumentJquery(resolveGlobalJquery()); } else if (config.jquery !== undefined && config.jquery !== false) { instrumentJquery(config.jquery); } if (config.exposeAppJourney) { exposeAppJourney( config.exposeAppJourney === true ? undefined : config.exposeAppJourney ); } } catch (err) { // eslint-disable-next-line no-console -- soft-fail host wiring console.error('[journey] configureJourney failed', err); } } /** * @internal Read the module runtime's app-wide slow-request config. * Prefer `configureJourney({ slowRequests })` to set thresholds. Test / introspection only. */ export function getSlowRequests(): SlowRequestConfig { return getSharedSlowRequests() ?? moduleRuntime.getSlowRequests(); }