|
| 1 | +# Metrics Module - Architecture |
| 2 | + |
| 3 | +> **Purpose**: Technical documentation for the metrics collection, batching, and submission system within the Contact Center SDK. |
| 4 | +
|
| 5 | +--- |
| 6 | + |
| 7 | +## Component Overview |
| 8 | + |
| 9 | +| Component | File | Responsibility | |
| 10 | +| ------------------------ | ----------------------- | ------------------------------------------------------------------------------ | |
| 11 | +| `MetricsManager` | `MetricsManager.ts` | Singleton that manages event queuing, timing, payload preparation, and submission | |
| 12 | +| `BehavioralEventTaxonomy`| `behavioral-events.ts` | Maps metric event names to structured taxonomy for behavioral analytics | |
| 13 | +| `METRIC_EVENT_NAMES` | `constants.ts` | Canonical constant object of all tracked metric event names | |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## File Structure |
| 18 | + |
| 19 | +``` |
| 20 | +src/metrics/ |
| 21 | +├── MetricsManager.ts # Singleton metrics manager |
| 22 | +├── behavioral-events.ts # Behavioral event taxonomy mapping |
| 23 | +├── constants.ts # METRIC_EVENT_NAMES constants |
| 24 | +└── ai-docs/ |
| 25 | + ├── AGENTS.md # Usage documentation |
| 26 | + └── ARCHITECTURE.md # This file |
| 27 | +``` |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## Singleton Pattern |
| 32 | + |
| 33 | +`MetricsManager` uses a private constructor with a static `getInstance` factory: |
| 34 | + |
| 35 | +```typescript |
| 36 | +// MetricsManager.ts |
| 37 | +export default class MetricsManager { |
| 38 | + private static instance: MetricsManager; |
| 39 | + private constructor() {} |
| 40 | + |
| 41 | + public static getInstance(options?: {webex: WebexSDK}): MetricsManager { |
| 42 | + if (!MetricsManager.instance) { |
| 43 | + MetricsManager.instance = new MetricsManager(); |
| 44 | + } |
| 45 | + if (!MetricsManager.instance.webex && options?.webex) { |
| 46 | + MetricsManager.instance.setWebex(options.webex); |
| 47 | + } |
| 48 | + return MetricsManager.instance; |
| 49 | + } |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +The Webex SDK instance is set once via `setWebex()`, which listens for the `ready` event before flushing pending queues. |
| 54 | + |
| 55 | +--- |
| 56 | + |
| 57 | +## Data Flow |
| 58 | + |
| 59 | +### Event Submission Flow |
| 60 | + |
| 61 | +```mermaid |
| 62 | +flowchart TD |
| 63 | + A[cc.ts calls metricsManager.timeEvent] --> B[Store startTime + keys in runningEvents] |
| 64 | + B --> C[Operation executes] |
| 65 | + C --> D{Success or Failure?} |
| 66 | + D -->|Success| E[cc.ts calls metricsManager.trackEvent with success name] |
| 67 | + D -->|Failure| F[cc.ts calls metricsManager.trackEvent with failure name] |
| 68 | + E --> G[addDurationIfTimed attaches duration_ms] |
| 69 | + F --> G |
| 70 | + G --> H[preparePayload cleans and enriches] |
| 71 | + H --> I{Metric type?} |
| 72 | + I -->|behavioral| J[Push to pendingBehavioralEvents] |
| 73 | + I -->|operational| K[Push to pendingOperationalEvents] |
| 74 | + I -->|business| L[Push to pendingBusinessEvents] |
| 75 | + J --> M[submitPendingBehavioralEvents] |
| 76 | + K --> N[submitPendingOperationalEvents] |
| 77 | + L --> O[submitPendingBusinessEvents] |
| 78 | + M --> P[webex.internal.newMetrics.submitBehavioralEvent] |
| 79 | + N --> Q[webex.internal.newMetrics.submitOperationalEvent] |
| 80 | + O --> R[webex.internal.newMetrics.submitBusinessEvent] |
| 81 | +``` |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Sequence Diagrams |
| 86 | + |
| 87 | +### Track Event with Timing |
| 88 | + |
| 89 | +```mermaid |
| 90 | +sequenceDiagram |
| 91 | + participant CC as ContactCenter (cc.ts) |
| 92 | + participant MM as MetricsManager |
| 93 | + participant NM as webex.internal.newMetrics |
| 94 | +
|
| 95 | + CC->>MM: timeEvent([SUCCESS_KEY, FAILURE_KEY]) |
| 96 | + Note over MM: Store startTime + key set in runningEvents |
| 97 | + CC->>CC: Execute operation (e.g., stationLogin) |
| 98 | + alt Success |
| 99 | + CC->>MM: trackEvent(SUCCESS_KEY, payload, ['behavioral', 'operational', 'business']) |
| 100 | + else Failure |
| 101 | + CC->>MM: trackEvent(FAILURE_KEY, payload, ['behavioral', 'operational', 'business']) |
| 102 | + end |
| 103 | + MM->>MM: addDurationIfTimed → attach duration_ms |
| 104 | + MM->>MM: preparePayload → clean empty fields, add tabHidden |
| 105 | + loop For each metric type |
| 106 | + MM->>MM: Push to pending queue |
| 107 | + alt readyToSubmitEvents |
| 108 | + MM->>NM: submit[Behavioral|Operational|Business]Event |
| 109 | + else not ready |
| 110 | + Note over MM: Events stay queued until SDK ready |
| 111 | + end |
| 112 | + end |
| 113 | +``` |
| 114 | + |
| 115 | +### Initialization and Readiness |
| 116 | + |
| 117 | +```mermaid |
| 118 | +sequenceDiagram |
| 119 | + participant CC as ContactCenter |
| 120 | + participant MM as MetricsManager |
| 121 | + participant Webex as WebexSDK |
| 122 | +
|
| 123 | + CC->>MM: getInstance({webex}) |
| 124 | + MM->>MM: Create singleton (if needed) |
| 125 | + MM->>MM: setWebex(webex) |
| 126 | + alt webex.ready === true |
| 127 | + MM->>MM: setReadyToSubmitEvents() |
| 128 | + MM->>MM: submitPendingEvents() |
| 129 | + else webex not ready yet |
| 130 | + MM->>Webex: webex.once('ready', callback) |
| 131 | + Note over MM: Events queue until ready |
| 132 | + Webex-->>MM: 'ready' event fires |
| 133 | + MM->>MM: setReadyToSubmitEvents() |
| 134 | + MM->>MM: submitPendingEvents() |
| 135 | + end |
| 136 | +``` |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +## Behavioral Event Taxonomy |
| 141 | + |
| 142 | +Each metric event name maps to a `BehavioralEventTaxonomy` with four fields: |
| 143 | + |
| 144 | +```typescript |
| 145 | +type BehavioralEventTaxonomy = { |
| 146 | + product: MetricEventProduct; // Always PRODUCT_NAME ('wxcc_sdk') |
| 147 | + agent: MetricEventAgent; // 'user' or 'service' |
| 148 | + target: string; // e.g., 'station_login', 'task_accept' |
| 149 | + verb: MetricEventVerb; // 'complete' for success, 'fail' for failure |
| 150 | +}; |
| 151 | +``` |
| 152 | + |
| 153 | +The final behavioral event name is constructed as: `{product}.{agent}.{target}.{verb}` |
| 154 | + |
| 155 | +Example: `wxcc_sdk.user.station_login.complete` |
| 156 | + |
| 157 | +The mapping is defined in `behavioral-events.ts` via `eventTaxonomyMap` and accessed through `getEventTaxonomy(name)`. |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +## Event Queuing and Submission |
| 162 | + |
| 163 | +### Three Parallel Queues |
| 164 | + |
| 165 | +MetricsManager maintains three independent pending event queues: |
| 166 | + |
| 167 | +| Queue | Type | Submitted Via | |
| 168 | +| --------------------------- | ------------ | -------------------------------------------------- | |
| 169 | +| `pendingBehavioralEvents` | behavioral | `webex.internal.newMetrics.submitBehavioralEvent` | |
| 170 | +| `pendingOperationalEvents` | operational | `webex.internal.newMetrics.submitOperationalEvent` | |
| 171 | +| `pendingBusinessEvents` | business | `webex.internal.newMetrics.submitBusinessEvent` | |
| 172 | + |
| 173 | +### Submission Guards |
| 174 | + |
| 175 | +- **readyToSubmitEvents**: Set to `true` only after `webex.once('ready')` fires. Events queue until then. |
| 176 | +- **submittingEvents**: Lock flag to prevent concurrent `submitPendingEvents()` calls. |
| 177 | +- **metricsDisabled**: When `true`, all `track*` methods return early and `clearPendingEvents()` empties all queues. |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## Timing Pattern (`timeEvent` / `trackEvent`) |
| 182 | + |
| 183 | +```mermaid |
| 184 | +flowchart LR |
| 185 | + A[timeEvent keys] --> B[runningEvents stores startTime + key Set] |
| 186 | + B --> C[trackEvent called with one of the keys] |
| 187 | + C --> D[addDurationIfTimed matches key] |
| 188 | + D --> E[Calculates duration_ms = now - startTime] |
| 189 | + E --> F[Removes all keys for that operation] |
| 190 | + F --> G[Attaches duration_ms to payload] |
| 191 | +``` |
| 192 | + |
| 193 | +Usage pattern from `cc.ts`: |
| 194 | + |
| 195 | +```typescript |
| 196 | +// Before operation |
| 197 | +this.metricsManager.timeEvent([ |
| 198 | + METRIC_EVENT_NAMES.STATION_LOGIN_SUCCESS, |
| 199 | + METRIC_EVENT_NAMES.STATION_LOGIN_FAILED, |
| 200 | +]); |
| 201 | + |
| 202 | +// On success |
| 203 | +this.metricsManager.trackEvent( |
| 204 | + METRIC_EVENT_NAMES.STATION_LOGIN_SUCCESS, |
| 205 | + { ...MetricsManager.getCommonTrackingFieldForAQMResponse(resp) }, |
| 206 | + ['behavioral', 'operational', 'business'] |
| 207 | +); |
| 208 | + |
| 209 | +// On failure |
| 210 | +this.metricsManager.trackEvent( |
| 211 | + METRIC_EVENT_NAMES.STATION_LOGIN_FAILED, |
| 212 | + { ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(failure) }, |
| 213 | + ['behavioral', 'operational', 'business'] |
| 214 | +); |
| 215 | +``` |
| 216 | + |
| 217 | +--- |
| 218 | + |
| 219 | +## Payload Preparation |
| 220 | + |
| 221 | +`preparePayload()` processes every event payload before submission: |
| 222 | + |
| 223 | +1. **Removes empty/null/undefined fields** — strips keys with `undefined`, `null`, `''`, any arrays, or empty objects |
| 224 | +2. **Converts spaces to underscores** — `spacesToUnderscore()` applied to all key names |
| 225 | +3. **Adds common metadata** — appends `tabHidden: document.hidden` in browser environments |
| 226 | + |
| 227 | +--- |
| 228 | + |
| 229 | +## Error Handling Strategy |
| 230 | + |
| 231 | +MetricsManager does not throw errors to callers. Instead: |
| 232 | + |
| 233 | +- Invalid metric types are logged via `LoggerProxy.error` |
| 234 | +- Empty `timeEvent` key arrays are logged and ignored |
| 235 | +- Disabled state (`metricsDisabled`) silently drops events |
| 236 | +- The `submittingEvents` lock prevents race conditions during concurrent submissions |
| 237 | + |
| 238 | +--- |
| 239 | + |
| 240 | +## Common Tracking Field Extraction |
| 241 | + |
| 242 | +Two static helpers extract standardized fields from AQM responses for metric payloads: |
| 243 | + |
| 244 | +### `getCommonTrackingFieldForAQMResponse(response)` |
| 245 | + |
| 246 | +Extracts: `agentId`, `agentSessionId`, `teamId`, `siteId`, `orgId`, `eventType`, `trackingId`, `notifTrackingId` |
| 247 | + |
| 248 | +### `getCommonTrackingFieldForAQMResponseFailed(failureResponse)` |
| 249 | + |
| 250 | +Extracts: `agentId`, `trackingId`, `notifTrackingId`, `orgId`, `failureType`, `failureReason`, `reasonCode` |
| 251 | + |
| 252 | +--- |
| 253 | + |
| 254 | +## Metric Event Names |
| 255 | + |
| 256 | +All event names are defined in `constants.ts` as `METRIC_EVENT_NAMES`. Events follow a `{Domain} {Action} {Success|Failed}` naming convention: |
| 257 | + |
| 258 | +| Category | Success Event | Failure Event | |
| 259 | +| ----------------- | -------------------------------------- | -------------------------------------- | |
| 260 | +| Agent Login | `STATION_LOGIN_SUCCESS` | `STATION_LOGIN_FAILED` | |
| 261 | +| Agent Logout | `STATION_LOGOUT_SUCCESS` | `STATION_LOGOUT_FAILED` | |
| 262 | +| Agent Relogin | `STATION_RELOGIN_SUCCESS` | `STATION_RELOGIN_FAILED` | |
| 263 | +| State Change | `AGENT_STATE_CHANGE_SUCCESS` | `AGENT_STATE_CHANGE_FAILED` | |
| 264 | +| Buddy Agents | `FETCH_BUDDY_AGENTS_SUCCESS` | `FETCH_BUDDY_AGENTS_FAILED` | |
| 265 | +| WebSocket | `WEBSOCKET_REGISTER_SUCCESS` | `WEBSOCKET_REGISTER_FAILED` | |
| 266 | +| Task Accept | `TASK_ACCEPT_SUCCESS` | `TASK_ACCEPT_FAILED` | |
| 267 | +| Task Hold | `TASK_HOLD_SUCCESS` | `TASK_HOLD_FAILED` | |
| 268 | +| Task Transfer | `TASK_TRANSFER_SUCCESS` | `TASK_TRANSFER_FAILED` | |
| 269 | +| Task Conference | `TASK_CONFERENCE_START_SUCCESS` | `TASK_CONFERENCE_START_FAILED` | |
| 270 | +| Outdial | `TASK_OUTDIAL_SUCCESS` | `TASK_OUTDIAL_FAILED` | |
| 271 | +| EntryPoint | `ENTRYPOINT_FETCH_SUCCESS` | `ENTRYPOINT_FETCH_FAILED` | |
| 272 | +| AddressBook | `ADDRESSBOOK_FETCH_SUCCESS` | `ADDRESSBOOK_FETCH_FAILED` | |
| 273 | +| Queue | `QUEUE_FETCH_SUCCESS` | `QUEUE_FETCH_FAILED` | |
| 274 | + |
| 275 | +Special events (no success/failure pair): `AGENT_RONA`, `AGENT_CONTACT_ASSIGN_FAILED`, `AGENT_INVITE_FAILED`, `WEBSOCKET_EVENT_RECEIVED` |
| 276 | + |
| 277 | +--- |
| 278 | + |
| 279 | +## Troubleshooting |
| 280 | + |
| 281 | +### Issue: Metrics not being submitted |
| 282 | + |
| 283 | +**Cause**: Webex SDK not yet ready when `trackEvent` is called |
| 284 | + |
| 285 | +**Solution**: Events are automatically queued in `pending*Events` arrays and flushed once `webex.once('ready')` fires. Verify the SDK is initializing correctly. |
| 286 | + |
| 287 | +### Issue: Duration not attached to metric |
| 288 | + |
| 289 | +**Cause**: `timeEvent` was not called before `trackEvent`, or the event name does not match any key in `runningEvents` |
| 290 | + |
| 291 | +**Solution**: Ensure `timeEvent([SUCCESS_KEY, FAILURE_KEY])` is called before the operation, and that the exact `METRIC_EVENT_NAMES` constant is used in both calls. |
| 292 | + |
| 293 | +### Issue: Metrics silently dropped |
| 294 | + |
| 295 | +**Cause**: `metricsDisabled` is set to `true` |
| 296 | + |
| 297 | +**Solution**: Check if `setMetricsDisabled(true)` was called. This clears all pending queues and causes all `track*` methods to return early. |
| 298 | + |
| 299 | +--- |
| 300 | + |
| 301 | +## Related Files |
| 302 | + |
| 303 | +- [MetricsManager.ts](../MetricsManager.ts) — Singleton metrics manager |
| 304 | +- [behavioral-events.ts](../behavioral-events.ts) — Event taxonomy mapping |
| 305 | +- [constants.ts](../constants.ts) — METRIC_EVENT_NAMES definitions |
| 306 | +- [cc.ts](../../cc.ts) — Main plugin class (primary consumer) |
| 307 | +- [constants.ts](../../constants.ts) — PRODUCT_NAME used in event prefixing |
0 commit comments