@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 +49 -5
- package/dist/index.d.ts +65 -1
- package/dist/index.js +190 -65
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/composable.test.ts +91 -16
- package/src/composable.ts +393 -80
- package/src/index.ts +7 -2
- package/src/lazySdk.test.ts +698 -0
- package/src/syntheticCapture.test.ts +74 -0
- package/src/syntheticCapture.ts +50 -0
package/src/composable.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
// TYPE-ONLY on purpose — this is the whole point of C-601's lazy path.
|
|
2
|
+
//
|
|
3
|
+
// A *value* import of the App Insights web SDK here folds the SDK into whatever
|
|
4
|
+
// chunk imports this module, which for a site is the ENTRY chunk. Measured on the
|
|
5
|
+
// bryans-handyman-solutions pilot (2026-08-11): adopting this package as published
|
|
6
|
+
// moved 192,265B raw / 77,920B gzip of SDK off its own async chunk and onto the
|
|
7
|
+
// entry chunk (+90.4% raw / +101.2% gzip), while the package's own code was only
|
|
8
|
+
// +4.8KB of that. Total bytes barely moved — the SDK simply relocated onto the
|
|
9
|
+
// critical path, defeating the sites' deliberate requestIdleCallback deferral.
|
|
10
|
+
//
|
|
11
|
+
// `import type` is erased by tsc/tsup, so it emits no runtime import at all. The
|
|
12
|
+
// SDK is reached exactly once, through the dynamic `import()` in `loadSdk()`
|
|
13
|
+
// below, which every bundler emits as a separate async chunk. Pinned by
|
|
14
|
+
// `lazySdk.test.ts` — reintroducing a value import here turns that spec red.
|
|
15
|
+
import type { ApplicationInsights } from '@microsoft/applicationinsights-web'
|
|
2
16
|
import { computed, ref } from 'vue'
|
|
3
17
|
|
|
4
18
|
import { DEFAULT_CLOUD_ROLE, createTelemetryConfig } from './config'
|
|
5
19
|
import { captureJourneyFirstTouch, setJourneyIdentityResolver } from './journey'
|
|
20
|
+
import { syntheticCaptureDetected } from './syntheticCapture'
|
|
6
21
|
import type {
|
|
7
22
|
TelemetryDependency,
|
|
8
23
|
TelemetryEvent,
|
|
@@ -28,6 +43,139 @@ let appInsights: ApplicationInsights | null = null
|
|
|
28
43
|
const isInitialized = ref(false)
|
|
29
44
|
const initializationError = ref<string | null>(null)
|
|
30
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The in-flight lazy SDK load, or `null` when none is running.
|
|
48
|
+
*
|
|
49
|
+
* This is the single-init guard AND the "is a load in flight" signal the pre-init
|
|
50
|
+
* buffer keys off. Two concurrent `initialize()` callers — a `main.ts` boot and a
|
|
51
|
+
* component's `onMounted`, say — must produce ONE dynamic import and ONE
|
|
52
|
+
* `new ApplicationInsights(...)`, or the site gets two SDK instances racing the
|
|
53
|
+
* same connection string. Mirrors the guarded initializer the sites already run
|
|
54
|
+
* (kduff-homes `useTelemetry.ts` `initializationPromise`).
|
|
55
|
+
*
|
|
56
|
+
* Non-null also means "buffer, don't drop": see {@link bufferWhileLoading}.
|
|
57
|
+
*/
|
|
58
|
+
let initPromise: Promise<boolean> | null = null
|
|
59
|
+
|
|
60
|
+
/** One already-built SDK call, replayed verbatim once the SDK arrives. */
|
|
61
|
+
type PendingSdkCall = (instance: ApplicationInsights) => void
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Calls made after `initialize()` but before the lazy SDK chunk resolves.
|
|
65
|
+
*
|
|
66
|
+
* Without this, laziness would silently cost data: a site that calls
|
|
67
|
+
* `initialize()` and immediately reports a landing pageView or a `cta_click`
|
|
68
|
+
* would have both dropped by the `!isInitialized` guards, because the SDK is
|
|
69
|
+
* still in flight over the network. The site-local composables this package
|
|
70
|
+
* replaces DO drop those calls (bryans/kduff both early-return) — buffering is a
|
|
71
|
+
* strict improvement on the pattern being adopted, not a port of it.
|
|
72
|
+
*
|
|
73
|
+
* Payloads are built at CALL time, so `timestamp`, `page_path` and `referrer_host`
|
|
74
|
+
* describe the moment the event happened, not the moment the chunk landed. Order
|
|
75
|
+
* is preserved.
|
|
76
|
+
*/
|
|
77
|
+
const pendingSdkCalls: PendingSdkCall[] = []
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Bound on the buffer. A load that never settles (offline, blocked CDN, an
|
|
81
|
+
* ad-blocker eating the SDK chunk) must not grow an unbounded array for the rest
|
|
82
|
+
* of the session on a long-lived SPA. 100 is far above the handful of events a
|
|
83
|
+
* real pre-init window sees and far below anything that matters for memory.
|
|
84
|
+
*/
|
|
85
|
+
const PENDING_CALL_LIMIT = 100
|
|
86
|
+
let warnedBufferFull = false
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Take ownership of a track* call while the SDK chunk is in flight.
|
|
90
|
+
*
|
|
91
|
+
* Returns `true` when this function has handled the call (buffered it, or
|
|
92
|
+
* deliberately dropped it because the buffer is full) and the caller must return.
|
|
93
|
+
* Returns `false` when no load is in flight — the caller then keeps its existing
|
|
94
|
+
* "not initialized" warn-and-drop behaviour, unchanged. That distinction is what
|
|
95
|
+
* stops an unconfigured site (never calls a successful `initialize()`) from
|
|
96
|
+
* accumulating a buffer nobody will ever drain.
|
|
97
|
+
*/
|
|
98
|
+
function bufferWhileLoading(call: PendingSdkCall): boolean {
|
|
99
|
+
if (!initPromise) return false
|
|
100
|
+
|
|
101
|
+
if (pendingSdkCalls.length >= PENDING_CALL_LIMIT) {
|
|
102
|
+
if (!warnedBufferFull) {
|
|
103
|
+
warnedBufferFull = true
|
|
104
|
+
console.warn(
|
|
105
|
+
`[telemetry] pre-init buffer full (${PENDING_CALL_LIMIT} calls) — dropping ` +
|
|
106
|
+
'telemetry until the Application Insights SDK chunk finishes loading. ' +
|
|
107
|
+
'If you see this, the SDK chunk is failing to load, not merely slow.',
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pendingSdkCalls.push(call)
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Replay everything buffered during the load, in order, then clear. */
|
|
118
|
+
function drainPendingSdkCalls(instance: ApplicationInsights): void {
|
|
119
|
+
const calls = pendingSdkCalls.splice(0, pendingSdkCalls.length)
|
|
120
|
+
for (const call of calls) {
|
|
121
|
+
try {
|
|
122
|
+
call(instance)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Failed to replay buffered telemetry:', error)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Abandon anything buffered — the SDK is never arriving. */
|
|
130
|
+
function discardPendingSdkCalls(): void {
|
|
131
|
+
pendingSdkCalls.length = 0
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Load the App Insights web SDK constructor on demand.
|
|
136
|
+
*
|
|
137
|
+
* The ONE runtime reference to `@microsoft/applicationinsights-web` in this
|
|
138
|
+
* package. Kept as a bare dynamic `import()` (not wrapped in a variable specifier)
|
|
139
|
+
* so bundlers can statically see the dependency and emit it as its own async
|
|
140
|
+
* chunk — which is exactly the shape the sites hand-rolled and the shape this
|
|
141
|
+
* package existed to fold up.
|
|
142
|
+
*/
|
|
143
|
+
async function loadSdk(): Promise<typeof ApplicationInsights> {
|
|
144
|
+
const module = await import('@microsoft/applicationinsights-web')
|
|
145
|
+
return module.ApplicationInsights
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Monotonic-ish clock for the pre-init page-timing pair. */
|
|
149
|
+
function nowMs(): number {
|
|
150
|
+
return typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
151
|
+
? performance.now()
|
|
152
|
+
: Date.now()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Pre-init `startTrackPage` timers, keyed by resolved page name.
|
|
157
|
+
*
|
|
158
|
+
* The SDK's own start/stop pair CANNOT be replayed faithfully after the fact: its
|
|
159
|
+
* internal timer would start at REPLAY time, so a page whose load straddled the
|
|
160
|
+
* lazy import would report a ~0ms duration. So a pair that straddles the load is
|
|
161
|
+
* timed here and replayed as the equivalent `trackPageView`.
|
|
162
|
+
*
|
|
163
|
+
* That is not an approximation — it is precisely what the SDK does internally.
|
|
164
|
+
* `AnalyticsPlugin.stopTrackPage` delegates to `_pageTracking.action`, which sets
|
|
165
|
+
* `properties.duration = duration.toString()` and calls `sendPageViewInternal`;
|
|
166
|
+
* `sendPageViewInternal` then derives `startTime` from `properties.duration` when
|
|
167
|
+
* no `startTime` is present (verified in applicationinsights-analytics-js 3.3.10,
|
|
168
|
+
* `dist-es5/JavaScriptSDK/AnalyticsPlugin.js`). Same wire shape, real duration —
|
|
169
|
+
* and, like the SDK, `duration` is left on the custom properties.
|
|
170
|
+
*/
|
|
171
|
+
const preInitPageTimings = new Map<string, number>()
|
|
172
|
+
|
|
173
|
+
/** The name the SDK itself would use: the argument, else `document.title`. */
|
|
174
|
+
function resolvePageName(name?: string): string {
|
|
175
|
+
if (typeof name === 'string') return name
|
|
176
|
+
return typeof document !== 'undefined' ? document.title : ''
|
|
177
|
+
}
|
|
178
|
+
|
|
31
179
|
// One-shot guards so we surface "App Insights is not configured" loudly in
|
|
32
180
|
// DevTools the FIRST time the app tries to use it, but don't spam the console
|
|
33
181
|
// for every subsequent trackEvent / trackPageView call. See web/ site-audit F2
|
|
@@ -144,6 +292,29 @@ function resolveAppVersion(explicit?: string): string {
|
|
|
144
292
|
* pass plain values in) so this package stays portable and build-tool agnostic.
|
|
145
293
|
* The shared App Insights config — including the `disablePageUnloadEvents:
|
|
146
294
|
* ['unload']` bfcache/Lighthouse fix — comes from {@link createTelemetryConfig}.
|
|
295
|
+
*
|
|
296
|
+
* ## The SDK loads LAZILY (C-601)
|
|
297
|
+
*
|
|
298
|
+
* `initialize()` still returns `boolean` synchronously — "telemetry is enabled and
|
|
299
|
+
* the SDK load has started" — but the App Insights web SDK itself arrives through
|
|
300
|
+
* a dynamic `import()`, so a bundler emits it as its own async chunk instead of
|
|
301
|
+
* folding ~192KB raw / ~78KB gzip into the consumer's entry chunk. Adopting this
|
|
302
|
+
* package therefore no longer moves the SDK onto the critical path.
|
|
303
|
+
*
|
|
304
|
+
* Three things make that safe:
|
|
305
|
+
*
|
|
306
|
+
* 1. **Buffered, not dropped.** track* calls made between `initialize()` and the
|
|
307
|
+
* chunk landing are queued with their call-time properties and replayed in
|
|
308
|
+
* order (bounded at {@link PENDING_CALL_LIMIT}). The site-local composables this
|
|
309
|
+
* package replaces dropped them.
|
|
310
|
+
* 2. **One init.** Concurrent `initialize()` callers share one `import()` and one
|
|
311
|
+
* `new ApplicationInsights(...)` via {@link initPromise}.
|
|
312
|
+
* 3. **Guards stay synchronous.** `enableAutoRouteTracking` is resolved before the
|
|
313
|
+
* `await`, so the C-288 double-count guard is armed for calls made during the
|
|
314
|
+
* load, not only after it.
|
|
315
|
+
*
|
|
316
|
+
* `whenReady()` is there for the rare caller that must sequence work after the SDK
|
|
317
|
+
* exists. You should not need it to avoid losing events.
|
|
147
318
|
*/
|
|
148
319
|
export function useTelemetry(options: TelemetryOptions = {}) {
|
|
149
320
|
const cloudRole = options.cloudRole ?? DEFAULT_CLOUD_ROLE
|
|
@@ -213,10 +384,27 @@ export function useTelemetry(options: TelemetryOptions = {}) {
|
|
|
213
384
|
// stored form submissions.
|
|
214
385
|
captureJourneyFirstTouch()
|
|
215
386
|
|
|
387
|
+
// C-614: the post-deploy snapshot-capture step walks the LIVE site with
|
|
388
|
+
// real Chromium. Declining BEFORE the SDK is constructed means a capture
|
|
389
|
+
// pass emits no pageView, event or exception at all — every track* call
|
|
390
|
+
// below already no-ops while `isInitialized` is false. Checked after the
|
|
391
|
+
// journey snapshot above so that deliberately unconditional behaviour is
|
|
392
|
+
// left exactly as it was.
|
|
393
|
+
if (syntheticCaptureDetected()) {
|
|
394
|
+
return false
|
|
395
|
+
}
|
|
396
|
+
|
|
216
397
|
if (isInitialized.value && appInsights) {
|
|
217
398
|
return true
|
|
218
399
|
}
|
|
219
400
|
|
|
401
|
+
// A load is already in flight (or already finished) — do NOT start a second
|
|
402
|
+
// one. Returning true keeps the caller's contract ("telemetry is on"); the
|
|
403
|
+
// awaitable answer is `whenReady()`.
|
|
404
|
+
if (initPromise) {
|
|
405
|
+
return true
|
|
406
|
+
}
|
|
407
|
+
|
|
220
408
|
if (!isEnabled.value) {
|
|
221
409
|
if (!warnedDisabled) {
|
|
222
410
|
warnedDisabled = true
|
|
@@ -243,33 +431,66 @@ export function useTelemetry(options: TelemetryOptions = {}) {
|
|
|
243
431
|
})
|
|
244
432
|
// Remember whether the SDK is counting page views for us, so trackPageView()
|
|
245
433
|
// can refuse to count them a second time.
|
|
434
|
+
//
|
|
435
|
+
// Resolved SYNCHRONOUSLY, before the lazy import, on purpose: the C-288
|
|
436
|
+
// double-count guard must be armed for the calls that arrive DURING the load,
|
|
437
|
+
// or a site with auto-route tracking on would buffer manual page views and
|
|
438
|
+
// then replay them into the duplicate the guard exists to prevent.
|
|
246
439
|
autoRouteTrackingActive = resolvedConfig.enableAutoRouteTracking !== false
|
|
247
440
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
appInsights.loadAppInsights()
|
|
251
|
-
appInsights.addTelemetryInitializer((envelope) => {
|
|
441
|
+
initPromise = (async (): Promise<boolean> => {
|
|
252
442
|
try {
|
|
253
|
-
|
|
254
|
-
} catch (error) {
|
|
255
|
-
console.warn('Failed to add web telemetry context:', error)
|
|
256
|
-
}
|
|
443
|
+
const ApplicationInsightsCtor = await loadSdk()
|
|
257
444
|
|
|
258
|
-
|
|
259
|
-
|
|
445
|
+
const instance = new ApplicationInsightsCtor({ config: resolvedConfig })
|
|
446
|
+
instance.loadAppInsights()
|
|
447
|
+
instance.addTelemetryInitializer((envelope) => {
|
|
448
|
+
try {
|
|
449
|
+
enrichTelemetryEnvelope(envelope as TelemetryEnvelope)
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.warn('Failed to add web telemetry context:', error)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return true
|
|
455
|
+
})
|
|
260
456
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
457
|
+
// Published LAST, after the SDK is wired: `appInsights` is what every
|
|
458
|
+
// track* guard reads, so assigning it earlier would let a call slip
|
|
459
|
+
// through to a half-configured instance (no telemetry initializer, so no
|
|
460
|
+
// app_version / cloud role on the envelope).
|
|
461
|
+
appInsights = instance
|
|
265
462
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
463
|
+
// Prefer the live SDK context over cookie parsing for the journey join
|
|
464
|
+
// key: the instance is authoritative, and it stays correct even if the
|
|
465
|
+
// cookies are blocked or renamed by a future SDK version.
|
|
466
|
+
setJourneyIdentityResolver(() => readSdkIdentity(appInsights))
|
|
269
467
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
468
|
+
isInitialized.value = true
|
|
469
|
+
initializationError.value = null
|
|
470
|
+
console.log('Application Insights initialized successfully')
|
|
471
|
+
|
|
472
|
+
// Anything that fired between initialize() and this moment, in order.
|
|
473
|
+
drainPendingSdkCalls(instance)
|
|
474
|
+
|
|
475
|
+
// Drain any conversion clicks captured before this transport existed, and
|
|
476
|
+
// stay attached for the rest of the session. See attachConversionCapture().
|
|
477
|
+
attachConversionCapture(trackEvent)
|
|
478
|
+
|
|
479
|
+
return true
|
|
480
|
+
} catch (error) {
|
|
481
|
+
const errorMessage =
|
|
482
|
+
error instanceof Error ? error.message : 'Unknown initialization error'
|
|
483
|
+
initializationError.value = errorMessage
|
|
484
|
+
console.error('Failed to initialize Application Insights:', error)
|
|
485
|
+
// The SDK is not coming. Release the buffer rather than holding events
|
|
486
|
+
// for a drain that will never happen, and clear the in-flight marker so
|
|
487
|
+
// subsequent track* calls warn-and-drop instead of buffering forever.
|
|
488
|
+
discardPendingSdkCalls()
|
|
489
|
+
preInitPageTimings.clear()
|
|
490
|
+
initPromise = null
|
|
491
|
+
return false
|
|
492
|
+
}
|
|
493
|
+
})()
|
|
273
494
|
|
|
274
495
|
return true
|
|
275
496
|
} catch (error) {
|
|
@@ -281,8 +502,34 @@ export function useTelemetry(options: TelemetryOptions = {}) {
|
|
|
281
502
|
}
|
|
282
503
|
}
|
|
283
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Resolves when the lazy App Insights chunk has finished loading (or failed).
|
|
507
|
+
*
|
|
508
|
+
* ADDITIVE — `initialize()` keeps its synchronous `boolean` return, so every
|
|
509
|
+
* existing caller (`web/src/main.ts`, the site adapters) is untouched. Use this
|
|
510
|
+
* when you genuinely need to sequence work after the SDK exists; you should not
|
|
511
|
+
* need it merely to avoid losing events, because calls made during the load are
|
|
512
|
+
* buffered and replayed.
|
|
513
|
+
*
|
|
514
|
+
* `false` means "no live SDK": telemetry is disabled, a synthetic capture pass
|
|
515
|
+
* declined it, `initialize()` was never called, or the chunk failed to load.
|
|
516
|
+
*/
|
|
517
|
+
const whenReady = (): Promise<boolean> => {
|
|
518
|
+
if (isInitialized.value && appInsights) return Promise.resolve(true)
|
|
519
|
+
return initPromise ?? Promise.resolve(false)
|
|
520
|
+
}
|
|
521
|
+
|
|
284
522
|
const trackEvent = (event: TelemetryEvent) => {
|
|
285
523
|
if (!appInsights || !isInitialized.value) {
|
|
524
|
+
// Built here, not at drain time, so the timestamp/page_path describe when the
|
|
525
|
+
// event happened rather than when the SDK chunk landed.
|
|
526
|
+
const payload = {
|
|
527
|
+
name: event.name,
|
|
528
|
+
properties: buildEventProperties(event.properties),
|
|
529
|
+
measurements: event.measurements,
|
|
530
|
+
}
|
|
531
|
+
if (bufferWhileLoading((instance) => instance.trackEvent(payload))) return
|
|
532
|
+
|
|
286
533
|
if (!warnedNotInitialized) {
|
|
287
534
|
warnedNotInitialized = true
|
|
288
535
|
console.warn(
|
|
@@ -318,11 +565,12 @@ export function useTelemetry(options: TelemetryOptions = {}) {
|
|
|
318
565
|
* `useTelemetry({ enableAutoRouteTracking: false })`. One tracker, one count.
|
|
319
566
|
*/
|
|
320
567
|
const trackPageView = (pageView?: TelemetryPageView) => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
568
|
+
// The double-count guard runs FIRST, before the init/buffer split. It is a
|
|
569
|
+
// question about CONFIG (is the SDK already counting routes?), not about load
|
|
570
|
+
// state, and `autoRouteTrackingActive` is resolved synchronously by
|
|
571
|
+
// initialize(). Checking it after the buffer would let manual page views queue
|
|
572
|
+
// during the load and then replay into the duplicate this guard exists to
|
|
573
|
+
// prevent (C-288: 52% of one site's page views).
|
|
326
574
|
if (autoRouteTrackingActive && !pageView?.force) {
|
|
327
575
|
if (!warnedDoubleCount) {
|
|
328
576
|
warnedDoubleCount = true
|
|
@@ -337,24 +585,31 @@ export function useTelemetry(options: TelemetryOptions = {}) {
|
|
|
337
585
|
return
|
|
338
586
|
}
|
|
339
587
|
|
|
588
|
+
// Attach the full document.referrer (not just hostname) so we can
|
|
589
|
+
// attribute traffic sources in App Insights pageViews.
|
|
590
|
+
const referrer = typeof document !== 'undefined' ? (document.referrer ?? '') : ''
|
|
591
|
+
const payload = {
|
|
592
|
+
name:
|
|
593
|
+
pageView?.name ||
|
|
594
|
+
(typeof document !== 'undefined' ? document.title : undefined),
|
|
595
|
+
uri:
|
|
596
|
+
pageView?.uri ||
|
|
597
|
+
(typeof window !== 'undefined' ? window.location.href : undefined),
|
|
598
|
+
properties: buildEventProperties({
|
|
599
|
+
referrer,
|
|
600
|
+
...pageView?.properties,
|
|
601
|
+
}),
|
|
602
|
+
measurements: pageView?.measurements,
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!appInsights || !isInitialized.value) {
|
|
606
|
+
if (bufferWhileLoading((instance) => instance.trackPageView(payload))) return
|
|
607
|
+
console.warn('Application Insights not initialized, skipping page view tracking')
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
|
|
340
611
|
try {
|
|
341
|
-
|
|
342
|
-
// attribute traffic sources in App Insights pageViews.
|
|
343
|
-
const referrer =
|
|
344
|
-
typeof document !== 'undefined' ? (document.referrer ?? '') : ''
|
|
345
|
-
appInsights.trackPageView({
|
|
346
|
-
name:
|
|
347
|
-
pageView?.name ||
|
|
348
|
-
(typeof document !== 'undefined' ? document.title : undefined),
|
|
349
|
-
uri:
|
|
350
|
-
pageView?.uri ||
|
|
351
|
-
(typeof window !== 'undefined' ? window.location.href : undefined),
|
|
352
|
-
properties: buildEventProperties({
|
|
353
|
-
referrer,
|
|
354
|
-
...pageView?.properties,
|
|
355
|
-
}),
|
|
356
|
-
measurements: pageView?.measurements,
|
|
357
|
-
})
|
|
612
|
+
appInsights.trackPageView(payload)
|
|
358
613
|
} catch (error) {
|
|
359
614
|
console.error('Failed to track page view:', error)
|
|
360
615
|
}
|
|
@@ -378,63 +633,75 @@ export function useTelemetry(options: TelemetryOptions = {}) {
|
|
|
378
633
|
}
|
|
379
634
|
|
|
380
635
|
const trackException = (exception: TelemetryException) => {
|
|
636
|
+
const payload = {
|
|
637
|
+
exception: exception.exception,
|
|
638
|
+
properties: buildEventProperties({
|
|
639
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
640
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
|
641
|
+
...exception.properties,
|
|
642
|
+
}),
|
|
643
|
+
measurements: exception.measurements,
|
|
644
|
+
}
|
|
645
|
+
|
|
381
646
|
if (!appInsights || !isInitialized.value) {
|
|
647
|
+
// Exceptions are the calls that most need the buffer: a boot-time error is
|
|
648
|
+
// exactly the class of failure that fires while the SDK chunk is still in
|
|
649
|
+
// flight, and it is the class you least want silently dropped.
|
|
650
|
+
if (bufferWhileLoading((instance) => instance.trackException(payload))) return
|
|
382
651
|
console.warn('Application Insights not initialized, skipping exception tracking')
|
|
383
652
|
return
|
|
384
653
|
}
|
|
385
654
|
|
|
386
655
|
try {
|
|
387
|
-
appInsights.trackException(
|
|
388
|
-
exception: exception.exception,
|
|
389
|
-
properties: buildEventProperties({
|
|
390
|
-
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
391
|
-
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
|
392
|
-
...exception.properties,
|
|
393
|
-
}),
|
|
394
|
-
measurements: exception.measurements,
|
|
395
|
-
})
|
|
656
|
+
appInsights.trackException(payload)
|
|
396
657
|
} catch (error) {
|
|
397
658
|
console.error('Failed to track exception:', error)
|
|
398
659
|
}
|
|
399
660
|
}
|
|
400
661
|
|
|
401
662
|
const trackDependency = (dependency: TelemetryDependency) => {
|
|
663
|
+
const payload = {
|
|
664
|
+
id: `dep-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
665
|
+
name: dependency.name,
|
|
666
|
+
data: dependency.data,
|
|
667
|
+
duration: dependency.duration,
|
|
668
|
+
success: dependency.success,
|
|
669
|
+
responseCode: dependency.resultCode || 0,
|
|
670
|
+
properties: buildEventProperties(dependency.properties),
|
|
671
|
+
measurements: dependency.measurements,
|
|
672
|
+
}
|
|
673
|
+
|
|
402
674
|
if (!appInsights || !isInitialized.value) {
|
|
675
|
+
if (bufferWhileLoading((instance) => instance.trackDependencyData(payload))) return
|
|
403
676
|
console.warn('Application Insights not initialized, skipping dependency tracking')
|
|
404
677
|
return
|
|
405
678
|
}
|
|
406
679
|
|
|
407
680
|
try {
|
|
408
|
-
appInsights.trackDependencyData(
|
|
409
|
-
id: `dep-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
410
|
-
name: dependency.name,
|
|
411
|
-
data: dependency.data,
|
|
412
|
-
duration: dependency.duration,
|
|
413
|
-
success: dependency.success,
|
|
414
|
-
responseCode: dependency.resultCode || 0,
|
|
415
|
-
properties: buildEventProperties(dependency.properties),
|
|
416
|
-
measurements: dependency.measurements,
|
|
417
|
-
})
|
|
681
|
+
appInsights.trackDependencyData(payload)
|
|
418
682
|
} catch (error) {
|
|
419
683
|
console.error('Failed to track dependency:', error)
|
|
420
684
|
}
|
|
421
685
|
}
|
|
422
686
|
|
|
423
687
|
const trackMetric = (metric: TelemetryMetric) => {
|
|
688
|
+
const payload = {
|
|
689
|
+
name: metric.name,
|
|
690
|
+
average: metric.average,
|
|
691
|
+
sampleCount: metric.sampleCount,
|
|
692
|
+
min: metric.min,
|
|
693
|
+
max: metric.max,
|
|
694
|
+
properties: buildEventProperties(metric.properties),
|
|
695
|
+
}
|
|
696
|
+
|
|
424
697
|
if (!appInsights || !isInitialized.value) {
|
|
698
|
+
if (bufferWhileLoading((instance) => instance.trackMetric(payload))) return
|
|
425
699
|
console.warn('Application Insights not initialized, skipping metric tracking')
|
|
426
700
|
return
|
|
427
701
|
}
|
|
428
702
|
|
|
429
703
|
try {
|
|
430
|
-
appInsights.trackMetric(
|
|
431
|
-
name: metric.name,
|
|
432
|
-
average: metric.average,
|
|
433
|
-
sampleCount: metric.sampleCount,
|
|
434
|
-
min: metric.min,
|
|
435
|
-
max: metric.max,
|
|
436
|
-
properties: buildEventProperties(metric.properties),
|
|
437
|
-
})
|
|
704
|
+
appInsights.trackMetric(payload)
|
|
438
705
|
} catch (error) {
|
|
439
706
|
console.error('Failed to track metric:', error)
|
|
440
707
|
}
|
|
@@ -478,13 +745,25 @@ export function useTelemetry(options: TelemetryOptions = {}) {
|
|
|
478
745
|
}
|
|
479
746
|
|
|
480
747
|
const startTrackPage = (name?: string) => {
|
|
481
|
-
|
|
748
|
+
// Redundancy first, for the same reason as trackPageView: it is a config
|
|
749
|
+
// question, answered synchronously, and it must hold for calls made during the
|
|
750
|
+
// lazy load too.
|
|
482
751
|
if (manualPageTrackingIsRedundant()) return
|
|
483
752
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
753
|
+
if (appInsights && isInitialized.value) {
|
|
754
|
+
try {
|
|
755
|
+
appInsights.startTrackPage(name)
|
|
756
|
+
} catch (error) {
|
|
757
|
+
console.error('Failed to start track page:', error)
|
|
758
|
+
}
|
|
759
|
+
return
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Time it ourselves while the SDK chunk is in flight — see preInitPageTimings.
|
|
763
|
+
// Outside a load (initialize() never called, or it failed) this drops silently,
|
|
764
|
+
// exactly as before.
|
|
765
|
+
if (initPromise) {
|
|
766
|
+
preInitPageTimings.set(resolvePageName(name), nowMs())
|
|
488
767
|
}
|
|
489
768
|
}
|
|
490
769
|
|
|
@@ -494,14 +773,47 @@ export function useTelemetry(options: TelemetryOptions = {}) {
|
|
|
494
773
|
properties?: Record<string, string>,
|
|
495
774
|
measurements?: Record<string, number>,
|
|
496
775
|
) => {
|
|
497
|
-
if (!appInsights || !isInitialized.value) return
|
|
498
776
|
if (manualPageTrackingIsRedundant()) return
|
|
499
777
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
778
|
+
if (appInsights && isInitialized.value) {
|
|
779
|
+
try {
|
|
780
|
+
appInsights.stopTrackPage(name, url, buildEventProperties(properties), measurements)
|
|
781
|
+
} catch (error) {
|
|
782
|
+
console.error('Failed to stop track page:', error)
|
|
783
|
+
}
|
|
784
|
+
return
|
|
504
785
|
}
|
|
786
|
+
|
|
787
|
+
if (!initPromise) return
|
|
788
|
+
|
|
789
|
+
const key = resolvePageName(name)
|
|
790
|
+
const startedAt = preInitPageTimings.get(key)
|
|
791
|
+
// No matching start means there is no timing to report — and inventing one
|
|
792
|
+
// would be worse than dropping it.
|
|
793
|
+
if (startedAt === undefined) return
|
|
794
|
+
preInitPageTimings.delete(key)
|
|
795
|
+
|
|
796
|
+
const duration = Math.max(0, Math.round(nowMs() - startedAt))
|
|
797
|
+
const payload = {
|
|
798
|
+
name: key,
|
|
799
|
+
uri: url ?? (typeof window !== 'undefined' ? window.location.href : undefined),
|
|
800
|
+
// `duration` as a STRING alongside the custom properties is the SDK's OWN
|
|
801
|
+
// encoding for a timed page view — `_pageTracking.action` does
|
|
802
|
+
// `properties.duration = duration.toString()` — and it is what
|
|
803
|
+
// `sendPageViewInternal` reads to back-date `startTime`.
|
|
804
|
+
//
|
|
805
|
+
// The cast is deliberate: `IPageViewTelemetry` declares `properties.duration`
|
|
806
|
+
// as `number`, which its own `stopTrackPage` path does not honour. Matching
|
|
807
|
+
// the runtime the collector actually parses beats matching a type that the
|
|
808
|
+
// emitting SDK contradicts.
|
|
809
|
+
properties: { ...buildEventProperties(properties), duration: String(duration) } as unknown as Record<
|
|
810
|
+
string,
|
|
811
|
+
number
|
|
812
|
+
>,
|
|
813
|
+
measurements,
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
bufferWhileLoading((instance) => instance.trackPageView(payload))
|
|
505
817
|
}
|
|
506
818
|
|
|
507
819
|
return {
|
|
@@ -509,6 +821,7 @@ export function useTelemetry(options: TelemetryOptions = {}) {
|
|
|
509
821
|
isInitialized,
|
|
510
822
|
initializationError,
|
|
511
823
|
initialize,
|
|
824
|
+
whenReady,
|
|
512
825
|
flush,
|
|
513
826
|
startTrackPage,
|
|
514
827
|
stopTrackPage,
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* `@duffcloudservices/telemetry` — shared Azure Application Insights telemetry
|
|
3
3
|
* for DCS Vue sites.
|
|
4
4
|
*
|
|
5
|
-
* - {@link useTelemetry} — the full composable (init + track* API).
|
|
6
|
-
*
|
|
5
|
+
* - {@link useTelemetry} — the full composable (init + track* API). Loads the App
|
|
6
|
+
* Insights web SDK through a dynamic `import()` (C-601), so importing this entry
|
|
7
|
+
* point costs the consumer NO SDK bytes on its entry chunk — the SDK becomes its
|
|
8
|
+
* own async chunk, exactly as the hand-rolled site composables arranged. Calls
|
|
9
|
+
* made while that chunk is in flight are buffered and replayed, so laziness does
|
|
10
|
+
* not cost events.
|
|
7
11
|
* - {@link createTelemetryConfig} — the shared App Insights config, with the
|
|
8
12
|
* `disablePageUnloadEvents: ['unload']` bfcache/Lighthouse fix baked into the
|
|
9
13
|
* default. Pure (no SDK import); also available SDK-free at
|
|
@@ -14,6 +18,7 @@
|
|
|
14
18
|
* telemetry boot.
|
|
15
19
|
*/
|
|
16
20
|
export { useTelemetry, type TelemetryApi } from './composable'
|
|
21
|
+
export { syntheticCaptureDetected, SYNTHETIC_CAPTURE_PARAMS } from './syntheticCapture'
|
|
17
22
|
export {
|
|
18
23
|
createTelemetryConfig,
|
|
19
24
|
DEFAULT_CLOUD_ROLE,
|