Applies to: src/prototype_1, src/prototype_2
Status: Accepted
Sprint: 2
See also: API Contract v1
This document defines the canonical event schema shared by both WatchTower prototypes. All SDK clients, servers, and dashboards must conform to this schema so that either candidate’s backend can serve either candidate’s frontend without modification.
Every event - regardless of type - uses the same outer envelope. The three required fields are set by the SDK before the event is queued; optional context fields are populated from SDK configuration; receivedAt is added server-side on ingestion.
| Field | Type | Description |
|---|---|---|
type |
string |
Event type (see Event Types). Must be a non-empty string. |
timestamp |
string |
ISO-8601 client-side creation time (e.g. "2026-05-16T18:00:00.000Z"). |
data |
object |
Type-specific payload (see per-type schemas below). Must be an object, never null. |
These fields are set by the SDK from its configuration and are present on every real event sent from a browser, but are not enforced by the server validator.
| Field | Type | Description |
|---|---|---|
sessionId |
string |
Stable per-tab identifier stored in sessionStorage. Used for active-user counting. |
userId |
string \| null |
Application-level user identifier. null until setUser() or trackLogin() is called. |
deployVersion |
string |
Deploy/release label (e.g. "v1.2.3"). Defaults to "unknown". |
appName |
string |
Logical application name. Defaults to location.hostname. |
url |
string |
Full location.href at the time the event was created. |
route |
string |
location.pathname at the time the event was created. |
| Field | Type | Description |
|---|---|---|
receivedAt |
string |
ISO-8601 server ingestion time, added by the server before the event is stored. |
{
"type": "click",
"timestamp": "2026-05-16T18:00:00.000Z",
"data": { "target": "button#submit", "text": "Submit" }
}
{
"type": "error",
"timestamp": "2026-05-16T18:00:01.234Z",
"sessionId": "a1b2c3d4-e5f6-4789",
"userId": "user_42",
"deployVersion": "v1.0.0",
"appName": "my-app.example.com",
"url": "https://my-app.example.com/dashboard",
"route": "/dashboard",
"data": {
"message": "Cannot read properties of undefined",
"source": "dashboard.js",
"line": 142,
"col": 8,
"stack": "TypeError: Cannot read properties of undefined\n at ..."
},
"receivedAt": "2026-05-16T18:00:01.300Z"
}
The server accepts any non-empty type string, but the dashboard only renders the following built-in types. New types can be added to the SDK without a coordinated server deploy.
errorCaptured automatically via window.onerror / unhandledrejection, or sent manually via WatchTower.trackError(error).
| Field | Type | Default | Description |
|---|---|---|---|
message |
string |
"Unknown error" |
Human-readable error message. |
source |
string |
"" |
Originating script filename or label ("manual" for manually tracked errors, "unhandledrejection" for promise rejections). |
line |
number |
0 |
Line number where the error occurred. |
col |
number |
0 |
Column number where the error occurred. |
stack |
string |
"" |
Stack trace string, if available. |
pageloadCaptured automatically on window.load using the Navigation Timing API (PerformanceNavigationTiming).
| Field | Type | Description |
|---|---|---|
duration |
number |
Total navigation duration in milliseconds (rounded). |
ttfb |
number |
Time to first byte in milliseconds (responseStart - requestStart, rounded). |
domContentLoaded |
number |
Time until DOMContentLoaded event fired in milliseconds. |
loadComplete |
number |
Time until load event fired in milliseconds. |
transferSize |
number |
Transfer size of the main document in bytes. 0 if unavailable. |
clickSent via WatchTower.trackClick(target, text).
| Field | Type | Description |
|---|---|---|
target |
string |
Description of the clicked element (e.g. CSS-like selector "button#submit"). |
text |
string |
Visible text on the element, truncated to 100 characters. |
loginSent via WatchTower.trackLogin(userId, method). Also sets userId on the SDK instance for all subsequent events.
| Field | Type | Default | Description |
|---|---|---|---|
userId |
string |
(required) | The user’s application-level identifier. |
method |
string |
"unknown" |
Authentication method label (e.g. "google", "email"). |
logoutSent via WatchTower.trackEvent("logout", payload) or custom integration. No standardized data fields; use data: {} or include app-specific context.
feedbackSent via WatchTower.trackEvent("feedback", feedbackData) or direct server POST. The server normalizes this type before storage.
| Field | Type | Constraints | Description |
|---|---|---|---|
rating |
number \| null |
Integer, clamped to [1, 5]. null if omitted or non-finite. |
User satisfaction rating. |
message |
string |
Trimmed, max 500 characters. | Free-form feedback text. |
category |
string |
Defaults to "general" if absent. |
Optional grouping label. |
customSent via WatchTower.trackEvent(name, payload).
| Field | Type | Description |
|---|---|---|
name |
string |
Short event name (e.g. "add-to-cart", "tutorial-complete"). |
payload |
object |
Arbitrary JSON-serializable details. Defaults to {}. |
The isValidEvent utility (in src/shared/utils/event-utils.js) enforces the minimum envelope check used in tests. The server itself stores events without re-validating them.
An event passes validation when:
type is a non-empty string.timestamp is a non-empty string that parses as a finite date (Date.parse returns a finite number).data is a non-null, non-undefined object.An event fails if any of the above is violated.
The SDK queues events locally and flushes in batches every 2 seconds (FLUSH_INTERVAL = 2000 ms). It also flushes immediately when document.visibilityState changes to "hidden" (tab switch or close). Failed batches are re-prepended to the queue for retry on the next flush cycle. Each flush sends at most 50 events.
| Version | Date | Notes |
|---|---|---|
| v1 | 2026-05-16 | Initial schema definition covering both prototypes. |