Skip to main content

Knowing What Operators Do

At 2 AM during a fire, nobody files a bug report. If a dispatcher fumbles with a control, clicks the wrong thing three times, or hits an error and just… works around it, the only witness is telemetry. That's what this chapter is about: the app streams every click, session, and error to Azure Application Insights, where we can replay what operators actually did — not what we hoped they'd do.

This lesson covers what gets tracked and how the pipeline works. Anatomy of a Click goes deep on the click tracker, and Asking Questions of the Data is the query cookbook.

What gets tracked​

Five streams, from broadest to most specific:

Every click. A global listener captures each click with rich context — what was clicked, what it said, where it sits in the component hierarchy, and every data-* attribute it carried. This is Dynatrace-style ambient capture: nobody has to remember to instrument a button for it to show up in the data.

Sessions. Start (browser, platform, screen size, timezone) and end (duration, page views, last action) — the envelope around everything else.

Identity. After sign-in, the user's id, organization, and role are attached to all subsequent telemetry, so "what did agency X's operators struggle with" is an answerable question.

Errors. Console errors, unhandled promise rejections, and React component crashes — with one deliberate filter: ArcGIS abort errors are excluded, because the map fires them constantly during normal pan/zoom and they'd drown everything else.

Domain events. Explicit calls for the things clicks can't express: camera interactions (view, PTZ, snapshot), map interactions (pan, zoom, layer toggles), alert interactions (create, acknowledge).

How a click becomes a queryable row​

Three things in that picture matter in practice. Events are batched, so don't expect one-network-request-per-click in the DevTools network tab. There's a 2–5 minute lag before events are queryable — patience before declaring your instrumentation broken. And retention is 90 days by default, so long-term analyses need their conclusions exported, not re-run.

Vitals: how the app feels, not just what was clicked​

Alongside events and page views, lib/vitals.ts reports how the app performs on the operator's machine, as custom metrics the Admin app's Vitals tab reads on demand:

  • Core Web Vitals (webvitals.LCP, INP, CLS, TTFB, FCP) from the web-vitals library, one row per page load or interaction, with its rating.
  • A responsiveness aggregate per visible minute (ui.longTasks, ui.blockedMs, ui.longTaskMax, ui.heapMB, ui.mountedTiles, ui.pfEngines): long tasks of 50 ms and up, main-thread blocked time, the worst freeze, JS heap, mounted camera tiles and live PeakFinder engines. ui.longTasks and ui.blockedMs are rates per visible minute; every row carries visibleSec (how much of the window the tab was visible) and reason (tick every 60 s, hidden when the tab is hidden or closed), so a 50 s visit and the last minute of a long session both count. Windows with under 5 s of visibility are not reported. The older long-task event only fires above 1 s, which is why a 300 ms stall every 10 s never appeared in telemetry.

Every row carries the build number as the envelope's application version and view (tile / location), deviceClass and viewport as properties, so any table can be split by release, screen and device. Reporting starts with the App Insights instance (startVitalsReporting in lib/applicationInsights.ts).

Where the code lives​

The whole system is two files plus a wiring point:

  • lib/applicationInsights.ts — the SDK setup and every track* function: initializeAppInsights, setUserContext/clearUserContext, trackClick, trackSessionStart/End, and the domain trackers (trackCameraInteraction, trackMapInteraction, trackAlertInteraction).
  • hooks/useAppInsightsTracking.ts — the React hooks: useClickTracking (the global listener), useSessionTracking (beforeunload), useUserTracking (auto-links auth state).
  • pages/_app.tsx — where the hooks are mounted, once, for the whole app.

Initialization is guarded by typeof window !== 'undefined' — the SDK never runs during SSR — and requires one env var, NEXT_PUBLIC_APPLICATIONINSIGHTS_CONNECTION_STRING. No connection string, no telemetry, no error: it fails quiet.

Wrapping up​

Every click, session, identity, error, and domain event flows through two files into Azure, batched and queryable within minutes. The system is ambient — you get click data for free — but the quality of that data depends on how components are written, which is exactly where the next lesson picks up: Anatomy of a Click.