@duffcloudservices/telemetry
| Install | |
|---|---|
npm install @duffcloudservices/telemetry |
|
| Latest Version: | 0.3.9 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 18, 2026 |
| Links: | npm |
@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 vue
The 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
Related Packages
Shared runtime for DCS customer sites — renders managed form definitions from .d...
Nuxt module to integrate OpenTelemetry (Azure Monitor) and Azure Application Ins...
DcsKit Vue runtime — editable primitives over editorBridge markers + the canonic...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.3.9 | ^3.4.0 | MIT | |
| 0.3.8 | ^3.4.0 | MIT | |
| 0.3.7 | ^3.4.0 | MIT | |
| 0.3.6 | ^3.4.0 | MIT | |
| 0.3.5 | ^3.4.0 | MIT | |
| 0.3.4 | ^3.4.0 | MIT | |
| 0.3.3 | ^3.4.0 | MIT | |
| 0.3.2 | ^3.4.0 | MIT | |
| 0.3.1 | ^3.4.0 | MIT | |
| 0.3.0 | ^3.4.0 | MIT | |
| 0.2.1 | ^3.4.0 | MIT | |
| 0.2.0 | ^3.4.0 | MIT | |
| 0.1.0 | ^3.4.0 | MIT |