@duffcloudservices/telemetry

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.
2,031
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 unload event 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 trackException is not lost.
  • initialize() is still synchronous and still returns boolean — "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>, so void initialize().then(…) must become initialize(); void whenReady().then(…). TypeScript flags the old form (Property 'then' does not exist on type 'boolean'), so this cannot slip through a site's type-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

@duffcloudservices/site-forms

Shared runtime for DCS customer sites — renders managed form definitions from .d...

4,646
nuxt-otel-appinsights

Nuxt module to integrate OpenTelemetry (Azure Monitor) and Azure Application Ins...

3,161
@duffcloudservices/kit

DcsKit Vue runtime — editable primitives over editorBridge markers + the canonic...

1,699
@duffcloudservices/cms

Vue 3 composables and Vite plugins for DCS CMS integration

10,104

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