@duffcloudservices/telemetry
v0.3.7
Published
Shared Azure Application Insights telemetry for DCS Vue sites — one App Insights config (with the bfcache-safe unload fix) and a useTelemetry composable that loads the App Insights SDK lazily, off the consumer's entry chunk.
Maintainers
Readme
@duffcloudservices/telemetry
Shared Azure Application Insights telemetry for DCS Vue sites. One place owns
the App Insights config and the useTelemetry composable, so every DCS surface
(marketing web/, generated customer sites) gets the same behavior — including
the fleet-wide bfcache / Lighthouse unload fix.
Why this package exists
The App Insights JavaScript SDK, by default, registers a listener on the
deprecated unload event. That:
- makes pages ineligible for the browser back/forward cache (bfcache), and
- trips the Lighthouse "avoid
unloadevent listeners" best-practices audit (~19 points — enough to pin flagship sites at Best-Practices 54).
The fix is a single config field — disablePageUnloadEvents: ['unload']
— which tells the SDK to skip the unload hook while still flushing telemetry on
pagehide / visibilitychange (the supported path). This package bakes that
into the default config so no consumer can forget it.
Install
pnpm add @duffcloudservices/telemetry
# peers you already have on a DCS Vue site:
pnpm add @microsoft/applicationinsights-web vueThe SDK loads lazily — adoption does not move it onto your entry chunk
Importing useTelemetry costs your entry chunk nothing but this package's own
code (~6–7KB raw / ~2KB gzip minified). The ~192KB raw / ~78KB gzip App Insights
web SDK is reached through a dynamic import() inside initialize(), so your
bundler emits it as its own async chunk, fetched only when initialize() runs.
This is not a micro-optimisation, it is the reason the package is adoptable. Before
0.3.0 the composable imported the SDK statically. Measured on a live customer site
(bryans-handyman-solutions, 2026-08-11), adopting 0.2.1 in place of the site's
hand-rolled lazy composable moved the whole SDK onto the entry chunk:
| | entry chunk raw | entry chunk gzip | SDK on critical path? |
| --- | --- | --- | --- |
| site's own lazy composable | 213,172 B | 76,398 B | no — separate async chunk |
| adopting 0.2.1 (static SDK) | 405,817 B (+90.4%) | 153,493 B (+101.2%) | yes |
| 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 |
Total transferred JS barely moved in the middle row — the SDK simply relocated onto
the critical path and defeated the site's deliberate requestIdleCallback deferral.
Laziness costs you no telemetry:
- Calls made during the load are buffered, with their call-time properties, and
replayed in order once the SDK lands (bounded at 100 calls). A landing pageView or
a boot-time
trackExceptionis not lost. initialize()is still synchronous and still returnsboolean— "telemetry is enabled and the SDK load has started". Nothing about the call site changes.- Concurrent
initialize()callers share one load and one SDK instance. whenReady(): Promise<boolean>is available for the rare caller that must sequence work after the SDK exists. You should not need it to avoid losing events.
Usage — composable (SPA / eager)
The connection string, cloud role, environment and app version are supplied by
the consumer (read your own import.meta.env / build defines and pass plain
values in), keeping this package portable.
import { useTelemetry } from '@duffcloudservices/telemetry'
const telemetry = useTelemetry({
connectionString: import.meta.env.VITE_APP_INSIGHTS_CONNECTION_STRING,
instrumentationKey: import.meta.env.VITE_APP_INSIGHTS_INSTRUMENT_KEY,
cloudRole: 'dcs-web',
dev: import.meta.env.DEV,
environment: import.meta.env.MODE,
appVersion: __APP_VERSION__,
})
telemetry.initialize() // synchronous; no-op (with a loud one-shot warn) when unconfigured
telemetry.trackCtaClick('hero-cta', '/contact') // buffered if the SDK is still loading
// Only when you must sequence work after the SDK itself exists:
void telemetry.whenReady().then(() => telemetry.trackPageView())initialize() returns boolean synchronously — the SDK loads in the background
(see above). Events fired before it lands are buffered, not dropped, so the common
case needs no await at all.
Migrating from a site-local composable? The site versions were
async initialize(): Promise<boolean>, sovoid initialize().then(…)must becomeinitialize(); void whenReady().then(…). TypeScript flags the old form (Property 'then' does not exist on type 'boolean'), so this cannot slip through a site'stype-check.
Usage — config only (lazy / inert loaders)
useTelemetry already loads the SDK lazily, so most sites no longer need this. It
remains for consumers that construct the SDK themselves — a bespoke boot, or a
non-Vue surface — and want the shared config without the composable:
import { createTelemetryConfig } from '@duffcloudservices/telemetry/config'
const { ApplicationInsights } = await import('@microsoft/applicationinsights-web')
const ai = new ApplicationInsights({
config: createTelemetryConfig({ connectionString: cs, enableAutoRouteTracking: false }),
})
ai.loadAppInsights()Usage — visitor journey on form submissions
A stored form submission and the site's telemetry describe the same visit,
but nothing joins them. getJourneyContext() returns the join key: the
App Insights visitor/session ids, the submit-time page, and the session's
first-touch referrer / landing path / utm_*.
First-touch is snapshotted into sessionStorage by initialize(), not
read at submit time — document.referrer is destroyed the moment a
client-side router takes over, so capture-at-submit is structurally empty
for exactly the multi-page journeys worth attributing.
The module imports nothing, so a consumer with its own telemetry boot can use it SDK-free:
import {
captureJourneyFirstTouch,
toJourneyTelemetryPayload,
} from '@duffcloudservices/telemetry/journey'
captureJourneyFirstTouch() // once, at app boot, before any navigation
const telemetry = toJourneyTelemetryPayload() // undefined when nothing to report
if (telemetry) payload.telemetry = telemetry // omit the field, never send {}A telemetry failure must never cost a lead. Every entry point swallows its own errors and degrades to an empty result; senders wrap the call and omit the field entirely rather than block a submission.
API
| Export | Description |
| --- | --- |
| 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. |
| createTelemetryConfig(options?) | Builds the App Insights config with DCS defaults + the disablePageUnloadEvents: ['unload'] fix. Pure, no SDK import. |
| getJourneyContext() | Visitor-journey join key for a form submission. Never throws. Also at @duffcloudservices/telemetry/journey (no SDK import). |
| toJourneyTelemetryPayload(context?) | Flattens a journey context into the DCS SubmissionTelemetry wire shape, or undefined when empty. |
| captureJourneyFirstTouch() | Snapshots this session's referrer / landing path / utm_*. Idempotent; called by initialize(). |
| setJourneyIdentityResolver(fn) | Registers a live-SDK source for visitor/session ids; falls back to the ai_user / ai_session cookies. |
| JOURNEY_FIELD_MAX_LENGTH | 512 — mirrors the server-side per-field clamp. |
| DEFAULT_DISABLE_PAGE_UNLOAD_EVENTS | ['unload'] — the disabled-events default. |
| DEFAULT_CLOUD_ROLE | 'dcs-site'. |
| types | TelemetryOptions, TelemetryConfigOptions, ApplicationInsightsConfig, TelemetryEvent, TelemetryPageView, TelemetryException, TelemetryDependency, TelemetryMetric, JourneyContext, JourneyTelemetryPayload, JourneyUtm, JourneyIdentityResolver. |
TelemetryOptions
| Option | Default | Notes |
| --- | --- | --- |
| connectionString / instrumentationKey | — | at least one enables telemetry |
| cloudRole | 'dcs-site' | ai.cloud.role + app_name property |
| appVersion | window.__APP_VERSION__ ?? 'unknown' | stamped on every event |
| environment | 'unknown' | e.g. import.meta.env.MODE |
| dev | false | verbose SDK logging when true |
| enableAutoRouteTracking | true | SDK built-in SPA route tracking |
| disablePageUnloadEvents | ['unload'] | the fix — override only if you know why |
| overrides | — | deep Partial<ApplicationInsightsConfig>, applied last |
License
MIT © Duff Cloud Services
