Observability and analytics
What exists, and what does not
| Channel | Status | Detail |
|---|---|---|
Segment ( |
Live |
|
Google Analytics (classic) |
Live |
Snippet injected under the global name |
Event logger ( |
Live, narrow |
One purpose: the KYC secure-site guardrail trail. See the guardrail trail. |
Redux DevTools |
Live in all builds |
|
Request ring buffer |
Live |
Last 50 requests in the |
Version reporting |
Live |
Front-end and back-end versions in the sidebar and on the login screen. |
VPN check |
Flag-gated |
|
Sentry |
Not wired up |
|
|
There is no error-tracking service. Production errors reach only the browser console
and, if uncaught, a Segment |
MonitorService
src/app/shared/monitor.service.ts is the analytics entry point. It does four
things.
1. Bootstraps Segment
It inlines the standard Segment snippet by hand — building the analytics stub
array, the method list, the factory, and finally:
analytics.load(this.segmentCode); // env.frontendSegmentIOCode
analytics.page();
The script URL is
https://cdn.segment.com/analytics.js/v1/<code>analytics.min.js. Note the write key
in the environment files ends with a / ('…zxA3/'), and set-env.ts appends
another / — the concatenation is deliberate.
2. Bootstraps Google Analytics
The classic GA snippet, assigned to the global fga rather than ga. Nothing in the
application calls it; it is present for whatever GA’s own auto-collection provides.
3. Identifies the user
initFullStory(): void {
this.store.select(selectLoggedUserModel)
.pipe(filter((u) => !!u))
.subscribe((loggedUser: UserModel) => {
window.analytics.identify(loggedUser.name, {
email: loggedUser.email,
displayName: loggedUser.name,
});
});
}
The method is named for FullStory but calls Segment. Note the identity passed is
loggedUser.name (a display name) rather than a stable id such as loginKey — so
Segment identities are not stable across a name change.
4. Maintains a message queue
send(message) → processQueue() → processMessage() → sendSegmentEvent(). The
queue and serviceBusy flag exist for a series-of-actions design that was never
finished — the commented-out EVENTS[message.subject].actions block with an
Async.eachLimit loop is still in the source, attributed in a comment.
sendSegmentEvent flattens nested data into dash-joined keys
(flatten(source, dest, '-', path)) and attaches screen dimensions as context:
window.analytics.track(message.subject, this.flatten(onboardingData), {
context: { screen: { width: screen.width, height: screen.height } },
});
The KYC guardrail audit trail
The one genuinely load-bearing piece of telemetry. MonitorService.send() only logs
for users holding a restricted permission:
public send(message?): void {
if (![USER_PERMISSION.REST_UW_ACCESS, USER_PERMISSION.ADVANCE_AUDITORS]
.includes(this.userPerrmision as USER_PERMISSION) || this.tracked) {
return;
}
this.logEvent(message);
this.tracked = !this.tracked;
}
logEvent sends both a Segment identify and an HTTP POST to
${eventLoggerUrl}track with:
body: { user, url: 'test`', target_type: 'supplier',
subject: 'Restricted UW Access',
extra: { route, eventTypeCode: 'kyc_secure_site_guardrails', suppKey? } }
headers: Authorization: 'Token 79e4933035c840438f999f8ad9e38571'
X-EventLogger-InstanceId: '8003'
X-EventLogger-Type: 'analitics'
X-EventLogger-TargetUUID: <suppKey> (when present)
So access to a supplier by a REST_UW_ACCESS or ADVANCE_AUDITORS user is recorded
as a kyc_secure_site_guardrails event against that supplier. That is the compliance
purpose of those two permissions' logging behaviour — see
authorisation.
|
Several problems are visible in this method, all known debt:
Treat the guardrail trail as best-effort, not as a reliable audit log. |
window.onerror
src/main.ts installs a last-resort handler outside Angular:
window.onerror = function (msg, url, lineNo, columnNo, error) {
try {
window.analytics.track('WINDOW ERROR', { msg, url, lineNo, columnNo, error });
} catch (err) {}
return false;
};
Returning false lets the default handler also run. The try/catch means a missing
analytics object cannot break it. This is the only automatic error reporting in
production.
ErrorLogger
shared/services/error-logger/error-logger.service.ts sends a Segment event with an
optional store dump, redacting six slices (auth, backgroundReports, calc,
crm, layout, utils).
What survives redaction includes supplier — which holds tax ids, owner SSNs and
addresses for every supplier visited — and errors, which holds the last 50
serialised requests.
|
Pass |
Version reporting
shared/version.service.ts:
getFrontendVersion(): string // environment.VERSION — from package.json at build
getBackendVersion(): Observable<string> // GET <internalApiUrl>version, shareReplay(1)
Surfaced in two places:
-
The sidebar footer —
F: <feVersion>andB: <beVersion>(data-test="sidebarFeVersion"/sidebarBeVersion). -
The login screen, before authentication.
semver is a dependency for comparing the two, and environment also carries
gitHash (the full 40-character commit) and lastBuildDate (ISO to the minute),
both stamped by deployment/set-env.ts. See
environments.
The stage banner
shared/layout/app-stage-info/ is designed to show which environment and backend you
are pointed at, including a truncated git hash (HASH_LENGTH_TO_SHOW = 8) and the
build date, and only on envName === 'test'.
|
It is currently disabled: The component uses |
The VPN check
shared/vpn-tester.service.ts, started by AppComponent when CHECK_VPN is
enabled:
private readonly DELAY = 3000; // first check after 3 s
private readonly INTERVAL = 30000; // then every 30 s
timer(DELAY, INTERVAL).pipe(
mergeMap(() => this.http.options(`${environment.pybDocumentsApiUrl}health-check`)
.pipe(mapTo(true), catchError(() => of(false)))),
).subscribe((hasVpn) => {
if (hasVpn) return;
this.toastrService.error(
'Please turn on the Payability VPN. Without it you may experience functionality issues');
});
It probes an internal host with an OPTIONS request every 30 seconds and toasts on
failure. Purely advisory — nothing is blocked. The real network restriction is
enforced upstream.
|
The subscription is never torn down, and it toasts on every failed check — so an
operator genuinely off-VPN accumulates a toast every 30 seconds. Also note
|
The Logger helper
src/app/shared/logger.ts — static, isDevMode()-gated console output with
distinctive group headers (------- SILENT LOG DEBUG --------,
!!!!!!! SILENT LOG ERROR !!!!!!!). Silent in production. Used by AuthEffects,
SupplierEffects and SseService.
Developer-facing observability
| Tool | Detail |
|---|---|
Redux DevTools |
Instrumented unconditionally — including production builds. The fastest way to see what the app is doing. |
Request ring buffer |
Last 50 requests (serialised, bodies included) in |
Feature-flag console dump |
|
Iframe message log |
|
Store debugger |
|
|
DevTools instrumentation plus the request ring buffer means a production build exposes the full store — including supplier PII and recent request bodies — to anyone with the browser extension. Accepted for an internal tool; worth knowing. |
Adding telemetry
-
Business event →
MonitorService.send({ subject, data }). Remember it only fires for restricted users; if you need it for everyone, callwindow.analytics.trackthrough a new method rather than reusingsend. -
Diagnostic detail →
Logger.log(dev-only). -
Error context →
ErrorLogger.log(title, data, false). -
Never add a hardcoded credential. If a new endpoint needs auth, it goes through
AuthInterceptorlike everything else — see HTTP layer.