@servicetitan/journey 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,7 @@ import {
11
11
  sortEndpointsByLongestMatch,
12
12
  type SlowRequestConfig,
13
13
  } from './endpoint-policy';
14
+ import { deserializeJourneyState, projectJourneyState } from './persist';
14
15
  import {
15
16
  clampMaxSteps,
16
17
  DEFAULT_MAX_STEPS,
@@ -36,6 +37,7 @@ import {
36
37
  type JourneyStepOptions,
37
38
  type JourneyStepTarget,
38
39
  type Outcome,
40
+ type SerializedJourneyState,
39
41
  type StepHandle,
40
42
  type StepRecord,
41
43
  type TagValue,
@@ -106,7 +108,8 @@ export interface JourneyRuntimeDebugSnapshot {
106
108
  };
107
109
  }
108
110
 
109
- function freezeJourneyDef(config: JourneyDef, tags: Record<string, TagValue>): JourneyDef {
111
+ /** @internal Freeze a caller-supplied journey def + tags snapshot. */
112
+ export function freezeJourneyDef(config: JourneyDef, tags: Record<string, TagValue>): JourneyDef {
110
113
  const endpoints = sortEndpointsByLongestMatch(config.endpoints);
111
114
  return Object.freeze({
112
115
  ...config,
@@ -164,6 +167,17 @@ export interface JourneyRuntime {
164
167
  completeJourneyState(journey: JourneyState, tags?: Record<string, TagValue>): void;
165
168
  excludeJourney(journey: JourneyState): void;
166
169
  updateOpenJourney(config: JourneyDef): void;
170
+ /**
171
+ * @internal Snapshot an open journey for cross-page transfer. Does NOT
172
+ * close the journey. Returns `null` when no journey is open under `name`.
173
+ */
174
+ serializeJourneyState(name: string): SerializedJourneyState | null;
175
+ /**
176
+ * @internal Rehydrate a serialized journey onto this runtime. Returns
177
+ * `null` (after emitting a `bad` `journey-timeout` event) when the journey
178
+ * already exhausted its budget in transit.
179
+ */
180
+ restoreJourney(serialized: SerializedJourneyState, config: JourneyDef): JourneyState | null;
167
181
 
168
182
  activeJourneyStep(): StepRecord | null;
169
183
  journeyStep<T>(
@@ -192,7 +206,8 @@ export interface JourneyRuntime {
192
206
  reset(options?: JourneyRuntimeOptions): void;
193
207
  }
194
208
 
195
- function bindJourneyEngine(
209
+ /** @internal Bind the fail/exclude/report hooks for a journey's creating engine. */
210
+ export function bindJourneyEngine(
196
211
  api: JourneyRuntime,
197
212
  journey: JourneyState,
198
213
  shouldIgnoreRequest: (target: JourneyState, key: string) => boolean
@@ -685,6 +700,51 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
685
700
  armCountdown(journey, remainingMs);
686
701
  },
687
702
 
703
+ serializeJourneyState(name) {
704
+ const journey = journeys.get(name);
705
+ if (!journey || journey.closed) {
706
+ return null;
707
+ }
708
+ return projectJourneyState(journey);
709
+ },
710
+
711
+ restoreJourney(serialized, config) {
712
+ // Refuse double-registration: leave an existing journey under this name in place.
713
+ if (journeys.has(config.name)) {
714
+ return null;
715
+ }
716
+
717
+ const journey = deserializeJourneyState(serialized, config, api, defaultJourneyIdleMs);
718
+
719
+ if (journey.timeoutMs > 0) {
720
+ const elapsedMs = globalThis.performance.now() - journey.startedAt;
721
+ if (elapsedMs >= journey.timeoutMs) {
722
+ // journey not yet in journeys Map — finish() handles unregistered journeys gracefully.
723
+ if (journey.timeoutExplicit) {
724
+ finish(journey, 'bad', 'journey-timeout');
725
+ } else {
726
+ finish(
727
+ journey,
728
+ journey.verdict === 'bad' ? 'bad' : 'excluded',
729
+ 'journey-idle-timeout'
730
+ );
731
+ }
732
+ return null;
733
+ }
734
+
735
+ journeys.set(config.name, journey);
736
+
737
+ const remainingMs = journey.timeoutMs - elapsedMs;
738
+ if (remainingMs > 0) {
739
+ armCountdown(journey, remainingMs);
740
+ }
741
+ } else {
742
+ journeys.set(config.name, journey);
743
+ }
744
+
745
+ return journey;
746
+ },
747
+
688
748
  activeJourneyStep() {
689
749
  if (!api.getAutoAttributeRequests()) {
690
750
  return null;
package/src/core/types.ts CHANGED
@@ -163,6 +163,32 @@ export interface JourneyStepEvent {
163
163
  attributes?: Record<string, TagValue>;
164
164
  }
165
165
 
166
+ /**
167
+ * Snapshot of an open journey that can survive a page navigation. Produced by
168
+ * `serializeJourneyState` and rehydrated by `restoreJourney`. Mirrors the
169
+ * `JourneyStepEvent` projection emitted by `finish()`, plus the wall-clock
170
+ * start time, verdict, reason, and tags needed to re-arm the countdown.
171
+ */
172
+ export interface SerializedJourneyState {
173
+ /** `st_journey_id` tag — cross-page correlation key. Empty string when unset. */
174
+ stJourneyId: string;
175
+ /** Wall-clock epoch (Date.now()) when the journey started. */
176
+ startedAtEpoch: number;
177
+ verdict: 'good' | 'bad';
178
+ reason: string | null;
179
+ steps: {
180
+ name: string;
181
+ /** Offset from the journey start, ms. */
182
+ startMs: number;
183
+ durationMs: number;
184
+ outcome: 'good' | 'bad';
185
+ reason?: string;
186
+ httpStatus?: number;
187
+ attributes?: Record<string, TagValue>;
188
+ }[];
189
+ tags: Record<string, TagValue>;
190
+ }
191
+
166
192
  /** Neutral journey event payload (journey-specific fields only; session context is host RUM globals). */
167
193
  export interface JourneyEvent {
168
194
  journey: {
package/src/index.ts CHANGED
@@ -13,6 +13,8 @@ export {
13
13
  excludeJourney,
14
14
  failJourney,
15
15
  journeyStep,
16
+ restoreJourney,
17
+ serializeJourneyState,
16
18
  setJourneyTags,
17
19
  // Custom transport adapters / request attribution.
18
20
  singleInFlightJourneyStep,
@@ -23,6 +25,7 @@ export {
23
25
  type JourneyNameLike,
24
26
  type JourneyStepOptions,
25
27
  type JourneyStepTarget,
28
+ type SerializedJourneyState,
26
29
  type StepHandle,
27
30
  } from './core';
28
31