@duffcloudservices/telemetry 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -29,6 +29,38 @@ pnpm add @duffcloudservices/telemetry
29
29
  pnpm add @microsoft/applicationinsights-web vue
30
30
  ```
31
31
 
32
+ ## The SDK loads lazily — adoption does not move it onto your entry chunk
33
+
34
+ Importing `useTelemetry` costs your **entry chunk** nothing but this package's own
35
+ code (~6–7KB raw / ~2KB gzip minified). The ~192KB raw / ~78KB gzip App Insights
36
+ web SDK is reached through a dynamic `import()` inside `initialize()`, so your
37
+ bundler emits it as its **own async chunk**, fetched only when `initialize()` runs.
38
+
39
+ This is not a micro-optimisation, it is the reason the package is adoptable. Before
40
+ `0.3.0` the composable imported the SDK statically. Measured on a live customer site
41
+ (bryans-handyman-solutions, 2026-08-11), adopting `0.2.1` in place of the site's
42
+ hand-rolled lazy composable moved the whole SDK onto the entry chunk:
43
+
44
+ | | entry chunk raw | entry chunk gzip | SDK on critical path? |
45
+ | --- | --- | --- | --- |
46
+ | site's own lazy composable | 213,172 B | 76,398 B | no — separate async chunk |
47
+ | adopting `0.2.1` (static SDK) | 405,817 B **(+90.4%)** | 153,493 B **(+101.2%)** | **yes** |
48
+ | adopting `0.3.0` (lazy SDK) | 219,842 B **(+3.1%)** | 78,582 B **(+2.9%)** | no — separate async chunk, byte-identical to the site's own |
49
+
50
+ Total transferred JS barely moved in the middle row — the SDK simply relocated onto
51
+ the critical path and defeated the site's deliberate `requestIdleCallback` deferral.
52
+
53
+ Laziness costs you no telemetry:
54
+
55
+ - **Calls made during the load are buffered**, with their call-time properties, and
56
+ replayed in order once the SDK lands (bounded at 100 calls). A landing pageView or
57
+ a boot-time `trackException` is not lost.
58
+ - **`initialize()` is still synchronous** and still returns `boolean` — "telemetry is
59
+ enabled and the SDK load has started". Nothing about the call site changes.
60
+ - **Concurrent `initialize()` callers share one load** and one SDK instance.
61
+ - `whenReady(): Promise<boolean>` is available for the rare caller that must
62
+ sequence work after the SDK exists. You should not need it to avoid losing events.
63
+
32
64
  ## Usage — composable (SPA / eager)
33
65
 
34
66
  The connection string, cloud role, environment and app version are supplied by
@@ -48,15 +80,27 @@ const telemetry = useTelemetry({
48
80
  })
49
81
 
50
82
  telemetry.initialize() // synchronous; no-op (with a loud one-shot warn) when unconfigured
51
- telemetry.trackCtaClick('hero-cta', '/contact')
83
+ telemetry.trackCtaClick('hero-cta', '/contact') // buffered if the SDK is still loading
84
+
85
+ // Only when you must sequence work after the SDK itself exists:
86
+ void telemetry.whenReady().then(() => telemetry.trackPageView())
52
87
  ```
53
88
 
54
- `initialize()` is **synchronous** because the SDK is statically imported.
89
+ `initialize()` returns `boolean` **synchronously** the SDK loads in the background
90
+ (see above). Events fired before it lands are buffered, not dropped, so the common
91
+ case needs no `await` at all.
92
+
93
+ > **Migrating from a site-local composable?** The site versions were
94
+ > `async initialize(): Promise<boolean>`, so `void initialize().then(…)` must become
95
+ > `initialize(); void whenReady().then(…)`. TypeScript flags the old form
96
+ > (`Property 'then' does not exist on type 'boolean'`), so this cannot slip through a
97
+ > site's `type-check`.
55
98
 
56
99
  ## Usage — config only (lazy / inert loaders)
57
100
 
58
- Sites that lazy-load the SDK themselves (to stay inert when telemetry is
59
- unconfigured) can import just the configno SDK is pulled in:
101
+ `useTelemetry` already loads the SDK lazily, so most sites no longer need this. It
102
+ remains for consumers that construct the SDK themselves a bespoke boot, or a
103
+ non-Vue surface — and want the shared config without the composable:
60
104
 
61
105
  ```ts
62
106
  import { createTelemetryConfig } from '@duffcloudservices/telemetry/config'
@@ -103,7 +147,7 @@ omit the field entirely rather than block a submission.
103
147
 
104
148
  | Export | Description |
105
149
  | --- | --- |
106
- | `useTelemetry(options?)` | Full composable: `initialize`, `trackEvent`, `trackPageView`, `trackCtaClick`, `trackException`, `trackMetric`, `trackDependency`, `flush`, `startTrackPage`, `stopTrackPage`, plus `isEnabled` / `isInitialized` / `initializationError` refs. |
150
+ | `useTelemetry(options?)` | Full composable: `initialize`, `whenReady`, `trackEvent`, `trackPageView`, `trackCtaClick`, `trackException`, `trackMetric`, `trackDependency`, `flush`, `startTrackPage`, `stopTrackPage`, plus `isEnabled` / `isInitialized` / `initializationError` refs. Loads the App Insights SDK lazily. |
107
151
  | `createTelemetryConfig(options?)` | Builds the App Insights config with DCS defaults + the `disablePageUnloadEvents: ['unload']` fix. Pure, no SDK import. |
108
152
  | `getJourneyContext()` | Visitor-journey join key for a form submission. Never throws. Also at `@duffcloudservices/telemetry/journey` (no SDK import). |
109
153
  | `toJourneyTelemetryPayload(context?)` | Flattens a journey context into the DCS `SubmissionTelemetry` wire shape, or `undefined` when empty. |
package/dist/index.d.ts CHANGED
@@ -12,12 +12,36 @@ import '@microsoft/applicationinsights-web';
12
12
  * pass plain values in) so this package stays portable and build-tool agnostic.
13
13
  * The shared App Insights config — including the `disablePageUnloadEvents:
14
14
  * ['unload']` bfcache/Lighthouse fix — comes from {@link createTelemetryConfig}.
15
+ *
16
+ * ## The SDK loads LAZILY (C-601)
17
+ *
18
+ * `initialize()` still returns `boolean` synchronously — "telemetry is enabled and
19
+ * the SDK load has started" — but the App Insights web SDK itself arrives through
20
+ * a dynamic `import()`, so a bundler emits it as its own async chunk instead of
21
+ * folding ~192KB raw / ~78KB gzip into the consumer's entry chunk. Adopting this
22
+ * package therefore no longer moves the SDK onto the critical path.
23
+ *
24
+ * Three things make that safe:
25
+ *
26
+ * 1. **Buffered, not dropped.** track* calls made between `initialize()` and the
27
+ * chunk landing are queued with their call-time properties and replayed in
28
+ * order (bounded at {@link PENDING_CALL_LIMIT}). The site-local composables this
29
+ * package replaces dropped them.
30
+ * 2. **One init.** Concurrent `initialize()` callers share one `import()` and one
31
+ * `new ApplicationInsights(...)` via {@link initPromise}.
32
+ * 3. **Guards stay synchronous.** `enableAutoRouteTracking` is resolved before the
33
+ * `await`, so the C-288 double-count guard is armed for calls made during the
34
+ * load, not only after it.
35
+ *
36
+ * `whenReady()` is there for the rare caller that must sequence work after the SDK
37
+ * exists. You should not need it to avoid losing events.
15
38
  */
16
39
  declare function useTelemetry(options?: TelemetryOptions): {
17
40
  isEnabled: vue.ComputedRef<boolean>;
18
41
  isInitialized: vue.Ref<boolean, boolean>;
19
42
  initializationError: vue.Ref<string | null, string | null>;
20
43
  initialize: () => boolean;
44
+ whenReady: () => Promise<boolean>;
21
45
  flush: () => void;
22
46
  startTrackPage: (name?: string) => void;
23
47
  stopTrackPage: (name?: string, url?: string, properties?: Record<string, string>, measurements?: Record<string, number>) => void;
@@ -31,4 +55,44 @@ declare function useTelemetry(options?: TelemetryOptions): {
31
55
  /** The full public API returned by {@link useTelemetry}. */
32
56
  type TelemetryApi = ReturnType<typeof useTelemetry>;
33
57
 
34
- export { type TelemetryApi, TelemetryDependency, TelemetryEvent, TelemetryException, TelemetryMetric, TelemetryOptions, TelemetryPageView, useTelemetry };
58
+ /**
59
+ * Synthetic-capture detection (C-614).
60
+ *
61
+ * The post-deploy "Capture page snapshots" CI step drives real Chromium against
62
+ * the LIVE production URL of the site it just deployed, so the site's telemetry
63
+ * boots exactly as it would for a visitor and every page the capture walked is
64
+ * recorded as a production pageView — during our own deploy run. Measured on
65
+ * coron8 over the 8 days to 2026-08-11: 49 synthetic pageViews against 14
66
+ * organic, 78% of the entire store, all 49 on 08-11 across 5 deploys versus 3
67
+ * real visits that day.
68
+ *
69
+ * The capture tooling marks every URL it loads (`packages/cli/src/commands/
70
+ * capture-snapshots.ts` and each site's own `scripts/capture-snapshots.*`).
71
+ * `initialize()` reads that mark and declines, so nothing is emitted at all.
72
+ * Suppressing here rather than tagging and filtering downstream keeps the
73
+ * denominator honest in every read path at once instead of obliging each of
74
+ * them to remember an exclusion — the C-430 preview/localhost blend is the
75
+ * recorded UNSOLVED residual that shape produces.
76
+ *
77
+ * DELIBERATE DUPLICATION. `@duffcloudservices/cms` carries the same predicate in
78
+ * its own `syntheticCapture.ts`. These two packages are intentionally decoupled
79
+ * — this one is the low-level telemetry package (peer deps: the App Insights SDK
80
+ * and vue, nothing else), cms is the site-content package — and a dependency
81
+ * edge between them to share ten lines of pure string matching would cost more
82
+ * than the duplication does. The parameter list is the contract; keep the two
83
+ * lists identical, and note that the CI drift guard in the canonical
84
+ * site-deploy workflow greps for these same names.
85
+ */
86
+ /** Query parameters that mark a document as an automated capture, not a visit. */
87
+ declare const SYNTHETIC_CAPTURE_PARAMS: readonly ["dcs-hide-ribbon", "dcs-no-telemetry"];
88
+ /**
89
+ * Whether this document is an automated capture rather than a visit worth counting.
90
+ *
91
+ * Reads `window.location.search` on every call rather than caching, so a
92
+ * client-side route change is judged on the CURRENT URL. Returns `false` on the
93
+ * server, and `false` when the query string cannot be read: over-suppression
94
+ * deletes real visits and leaves no trace to notice it by, so this fails OPEN.
95
+ */
96
+ declare function syntheticCaptureDetected(): boolean;
97
+
98
+ export { SYNTHETIC_CAPTURE_PARAMS, type TelemetryApi, TelemetryDependency, TelemetryEvent, TelemetryException, TelemetryMetric, TelemetryOptions, TelemetryPageView, syntheticCaptureDetected, useTelemetry };
package/dist/index.js CHANGED
@@ -2,12 +2,67 @@ import { DEFAULT_CLOUD_ROLE, createTelemetryConfig } from './chunk-MX6WFWG4.js';
2
2
  export { DEFAULT_CLOUD_ROLE, DEFAULT_DISABLE_PAGE_UNLOAD_EVENTS, createTelemetryConfig } from './chunk-MX6WFWG4.js';
3
3
  import { captureJourneyFirstTouch, setJourneyIdentityResolver } from './chunk-DKX4AC2U.js';
4
4
  export { JOURNEY_FIELD_MAX_LENGTH, JOURNEY_FIRST_TOUCH_KEY, captureJourneyFirstTouch, getJourneyContext, setJourneyIdentityResolver, toJourneyTelemetryPayload } from './chunk-DKX4AC2U.js';
5
- import { ApplicationInsights } from '@microsoft/applicationinsights-web';
6
5
  import { ref, computed } from 'vue';
7
6
 
7
+ // src/syntheticCapture.ts
8
+ var SYNTHETIC_CAPTURE_PARAMS = ["dcs-hide-ribbon", "dcs-no-telemetry"];
9
+ function syntheticCaptureDetected() {
10
+ if (typeof window === "undefined") return false;
11
+ try {
12
+ const params = new URLSearchParams(window.location.search);
13
+ return SYNTHETIC_CAPTURE_PARAMS.some((param) => params.has(param));
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ // src/composable.ts
8
20
  var appInsights = null;
9
21
  var isInitialized = ref(false);
10
22
  var initializationError = ref(null);
23
+ var initPromise = null;
24
+ var pendingSdkCalls = [];
25
+ var PENDING_CALL_LIMIT = 100;
26
+ var warnedBufferFull = false;
27
+ function bufferWhileLoading(call) {
28
+ if (!initPromise) return false;
29
+ if (pendingSdkCalls.length >= PENDING_CALL_LIMIT) {
30
+ if (!warnedBufferFull) {
31
+ warnedBufferFull = true;
32
+ console.warn(
33
+ `[telemetry] pre-init buffer full (${PENDING_CALL_LIMIT} calls) \u2014 dropping telemetry until the Application Insights SDK chunk finishes loading. If you see this, the SDK chunk is failing to load, not merely slow.`
34
+ );
35
+ }
36
+ return true;
37
+ }
38
+ pendingSdkCalls.push(call);
39
+ return true;
40
+ }
41
+ function drainPendingSdkCalls(instance) {
42
+ const calls = pendingSdkCalls.splice(0, pendingSdkCalls.length);
43
+ for (const call of calls) {
44
+ try {
45
+ call(instance);
46
+ } catch (error) {
47
+ console.error("Failed to replay buffered telemetry:", error);
48
+ }
49
+ }
50
+ }
51
+ function discardPendingSdkCalls() {
52
+ pendingSdkCalls.length = 0;
53
+ }
54
+ async function loadSdk() {
55
+ const module = await import('@microsoft/applicationinsights-web');
56
+ return module.ApplicationInsights;
57
+ }
58
+ function nowMs() {
59
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
60
+ }
61
+ var preInitPageTimings = /* @__PURE__ */ new Map();
62
+ function resolvePageName(name) {
63
+ if (typeof name === "string") return name;
64
+ return typeof document !== "undefined" ? document.title : "";
65
+ }
11
66
  var warnedDisabled = false;
12
67
  var warnedNotInitialized = false;
13
68
  var warnedDoubleCount = false;
@@ -91,9 +146,15 @@ function useTelemetry(options = {}) {
91
146
  const initialize = () => {
92
147
  try {
93
148
  captureJourneyFirstTouch();
149
+ if (syntheticCaptureDetected()) {
150
+ return false;
151
+ }
94
152
  if (isInitialized.value && appInsights) {
95
153
  return true;
96
154
  }
155
+ if (initPromise) {
156
+ return true;
157
+ }
97
158
  if (!isEnabled.value) {
98
159
  if (!warnedDisabled) {
99
160
  warnedDisabled = true;
@@ -112,21 +173,37 @@ function useTelemetry(options = {}) {
112
173
  overrides: options.overrides
113
174
  });
114
175
  autoRouteTrackingActive = resolvedConfig.enableAutoRouteTracking !== false;
115
- appInsights = new ApplicationInsights({ config: resolvedConfig });
116
- appInsights.loadAppInsights();
117
- appInsights.addTelemetryInitializer((envelope) => {
176
+ initPromise = (async () => {
118
177
  try {
119
- enrichTelemetryEnvelope(envelope);
178
+ const ApplicationInsightsCtor = await loadSdk();
179
+ const instance = new ApplicationInsightsCtor({ config: resolvedConfig });
180
+ instance.loadAppInsights();
181
+ instance.addTelemetryInitializer((envelope) => {
182
+ try {
183
+ enrichTelemetryEnvelope(envelope);
184
+ } catch (error) {
185
+ console.warn("Failed to add web telemetry context:", error);
186
+ }
187
+ return true;
188
+ });
189
+ appInsights = instance;
190
+ setJourneyIdentityResolver(() => readSdkIdentity(appInsights));
191
+ isInitialized.value = true;
192
+ initializationError.value = null;
193
+ console.log("Application Insights initialized successfully");
194
+ drainPendingSdkCalls(instance);
195
+ attachConversionCapture(trackEvent);
196
+ return true;
120
197
  } catch (error) {
121
- console.warn("Failed to add web telemetry context:", error);
198
+ const errorMessage = error instanceof Error ? error.message : "Unknown initialization error";
199
+ initializationError.value = errorMessage;
200
+ console.error("Failed to initialize Application Insights:", error);
201
+ discardPendingSdkCalls();
202
+ preInitPageTimings.clear();
203
+ initPromise = null;
204
+ return false;
122
205
  }
123
- return true;
124
- });
125
- setJourneyIdentityResolver(() => readSdkIdentity(appInsights));
126
- isInitialized.value = true;
127
- initializationError.value = null;
128
- console.log("Application Insights initialized successfully");
129
- attachConversionCapture(trackEvent);
206
+ })();
130
207
  return true;
131
208
  } catch (error) {
132
209
  const errorMessage = error instanceof Error ? error.message : "Unknown initialization error";
@@ -135,8 +212,18 @@ function useTelemetry(options = {}) {
135
212
  return false;
136
213
  }
137
214
  };
215
+ const whenReady = () => {
216
+ if (isInitialized.value && appInsights) return Promise.resolve(true);
217
+ return initPromise ?? Promise.resolve(false);
218
+ };
138
219
  const trackEvent = (event) => {
139
220
  if (!appInsights || !isInitialized.value) {
221
+ const payload = {
222
+ name: event.name,
223
+ properties: buildEventProperties(event.properties),
224
+ measurements: event.measurements
225
+ };
226
+ if (bufferWhileLoading((instance) => instance.trackEvent(payload))) return;
140
227
  if (!warnedNotInitialized) {
141
228
  warnedNotInitialized = true;
142
229
  console.warn(
@@ -156,10 +243,6 @@ function useTelemetry(options = {}) {
156
243
  }
157
244
  };
158
245
  const trackPageView = (pageView) => {
159
- if (!appInsights || !isInitialized.value) {
160
- console.warn("Application Insights not initialized, skipping page view tracking");
161
- return;
162
- }
163
246
  if (autoRouteTrackingActive && !pageView?.force) {
164
247
  if (!warnedDoubleCount) {
165
248
  warnedDoubleCount = true;
@@ -169,17 +252,23 @@ function useTelemetry(options = {}) {
169
252
  }
170
253
  return;
171
254
  }
255
+ const referrer = typeof document !== "undefined" ? document.referrer ?? "" : "";
256
+ const payload = {
257
+ name: pageView?.name || (typeof document !== "undefined" ? document.title : void 0),
258
+ uri: pageView?.uri || (typeof window !== "undefined" ? window.location.href : void 0),
259
+ properties: buildEventProperties({
260
+ referrer,
261
+ ...pageView?.properties
262
+ }),
263
+ measurements: pageView?.measurements
264
+ };
265
+ if (!appInsights || !isInitialized.value) {
266
+ if (bufferWhileLoading((instance) => instance.trackPageView(payload))) return;
267
+ console.warn("Application Insights not initialized, skipping page view tracking");
268
+ return;
269
+ }
172
270
  try {
173
- const referrer = typeof document !== "undefined" ? document.referrer ?? "" : "";
174
- appInsights.trackPageView({
175
- name: pageView?.name || (typeof document !== "undefined" ? document.title : void 0),
176
- uri: pageView?.uri || (typeof window !== "undefined" ? window.location.href : void 0),
177
- properties: buildEventProperties({
178
- referrer,
179
- ...pageView?.properties
180
- }),
181
- measurements: pageView?.measurements
182
- });
271
+ appInsights.trackPageView(payload);
183
272
  } catch (error) {
184
273
  console.error("Failed to track page view:", error);
185
274
  }
@@ -194,58 +283,64 @@ function useTelemetry(options = {}) {
194
283
  });
195
284
  };
196
285
  const trackException = (exception) => {
286
+ const payload = {
287
+ exception: exception.exception,
288
+ properties: buildEventProperties({
289
+ url: typeof window !== "undefined" ? window.location.href : "",
290
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
291
+ ...exception.properties
292
+ }),
293
+ measurements: exception.measurements
294
+ };
197
295
  if (!appInsights || !isInitialized.value) {
296
+ if (bufferWhileLoading((instance) => instance.trackException(payload))) return;
198
297
  console.warn("Application Insights not initialized, skipping exception tracking");
199
298
  return;
200
299
  }
201
300
  try {
202
- appInsights.trackException({
203
- exception: exception.exception,
204
- properties: buildEventProperties({
205
- url: typeof window !== "undefined" ? window.location.href : "",
206
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
207
- ...exception.properties
208
- }),
209
- measurements: exception.measurements
210
- });
301
+ appInsights.trackException(payload);
211
302
  } catch (error) {
212
303
  console.error("Failed to track exception:", error);
213
304
  }
214
305
  };
215
306
  const trackDependency = (dependency) => {
307
+ const payload = {
308
+ id: `dep-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
309
+ name: dependency.name,
310
+ data: dependency.data,
311
+ duration: dependency.duration,
312
+ success: dependency.success,
313
+ responseCode: dependency.resultCode || 0,
314
+ properties: buildEventProperties(dependency.properties),
315
+ measurements: dependency.measurements
316
+ };
216
317
  if (!appInsights || !isInitialized.value) {
318
+ if (bufferWhileLoading((instance) => instance.trackDependencyData(payload))) return;
217
319
  console.warn("Application Insights not initialized, skipping dependency tracking");
218
320
  return;
219
321
  }
220
322
  try {
221
- appInsights.trackDependencyData({
222
- id: `dep-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
223
- name: dependency.name,
224
- data: dependency.data,
225
- duration: dependency.duration,
226
- success: dependency.success,
227
- responseCode: dependency.resultCode || 0,
228
- properties: buildEventProperties(dependency.properties),
229
- measurements: dependency.measurements
230
- });
323
+ appInsights.trackDependencyData(payload);
231
324
  } catch (error) {
232
325
  console.error("Failed to track dependency:", error);
233
326
  }
234
327
  };
235
328
  const trackMetric = (metric) => {
329
+ const payload = {
330
+ name: metric.name,
331
+ average: metric.average,
332
+ sampleCount: metric.sampleCount,
333
+ min: metric.min,
334
+ max: metric.max,
335
+ properties: buildEventProperties(metric.properties)
336
+ };
236
337
  if (!appInsights || !isInitialized.value) {
338
+ if (bufferWhileLoading((instance) => instance.trackMetric(payload))) return;
237
339
  console.warn("Application Insights not initialized, skipping metric tracking");
238
340
  return;
239
341
  }
240
342
  try {
241
- appInsights.trackMetric({
242
- name: metric.name,
243
- average: metric.average,
244
- sampleCount: metric.sampleCount,
245
- min: metric.min,
246
- max: metric.max,
247
- properties: buildEventProperties(metric.properties)
248
- });
343
+ appInsights.trackMetric(payload);
249
344
  } catch (error) {
250
345
  console.error("Failed to track metric:", error);
251
346
  }
@@ -269,28 +364,58 @@ function useTelemetry(options = {}) {
269
364
  return true;
270
365
  };
271
366
  const startTrackPage = (name) => {
272
- if (!appInsights || !isInitialized.value) return;
273
367
  if (manualPageTrackingIsRedundant()) return;
274
- try {
275
- appInsights.startTrackPage(name);
276
- } catch (error) {
277
- console.error("Failed to start track page:", error);
368
+ if (appInsights && isInitialized.value) {
369
+ try {
370
+ appInsights.startTrackPage(name);
371
+ } catch (error) {
372
+ console.error("Failed to start track page:", error);
373
+ }
374
+ return;
375
+ }
376
+ if (initPromise) {
377
+ preInitPageTimings.set(resolvePageName(name), nowMs());
278
378
  }
279
379
  };
280
380
  const stopTrackPage = (name, url, properties, measurements) => {
281
- if (!appInsights || !isInitialized.value) return;
282
381
  if (manualPageTrackingIsRedundant()) return;
283
- try {
284
- appInsights.stopTrackPage(name, url, buildEventProperties(properties), measurements);
285
- } catch (error) {
286
- console.error("Failed to stop track page:", error);
382
+ if (appInsights && isInitialized.value) {
383
+ try {
384
+ appInsights.stopTrackPage(name, url, buildEventProperties(properties), measurements);
385
+ } catch (error) {
386
+ console.error("Failed to stop track page:", error);
387
+ }
388
+ return;
287
389
  }
390
+ if (!initPromise) return;
391
+ const key = resolvePageName(name);
392
+ const startedAt = preInitPageTimings.get(key);
393
+ if (startedAt === void 0) return;
394
+ preInitPageTimings.delete(key);
395
+ const duration = Math.max(0, Math.round(nowMs() - startedAt));
396
+ const payload = {
397
+ name: key,
398
+ uri: url ?? (typeof window !== "undefined" ? window.location.href : void 0),
399
+ // `duration` as a STRING alongside the custom properties is the SDK's OWN
400
+ // encoding for a timed page view — `_pageTracking.action` does
401
+ // `properties.duration = duration.toString()` — and it is what
402
+ // `sendPageViewInternal` reads to back-date `startTime`.
403
+ //
404
+ // The cast is deliberate: `IPageViewTelemetry` declares `properties.duration`
405
+ // as `number`, which its own `stopTrackPage` path does not honour. Matching
406
+ // the runtime the collector actually parses beats matching a type that the
407
+ // emitting SDK contradicts.
408
+ properties: { ...buildEventProperties(properties), duration: String(duration) },
409
+ measurements
410
+ };
411
+ bufferWhileLoading((instance) => instance.trackPageView(payload));
288
412
  };
289
413
  return {
290
414
  isEnabled,
291
415
  isInitialized,
292
416
  initializationError,
293
417
  initialize,
418
+ whenReady,
294
419
  flush,
295
420
  startTrackPage,
296
421
  stopTrackPage,
@@ -303,6 +428,6 @@ function useTelemetry(options = {}) {
303
428
  };
304
429
  }
305
430
 
306
- export { useTelemetry };
431
+ export { SYNTHETIC_CAPTURE_PARAMS, syntheticCaptureDetected, useTelemetry };
307
432
  //# sourceMappingURL=index.js.map
308
433
  //# sourceMappingURL=index.js.map