@webex/internal-plugin-metrics 3.12.0-next.6 → 3.12.0-next.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.repo-context.json +466 -0
  2. package/.sdd/manifest.json +626 -0
  3. package/AGENTS.md +208 -0
  4. package/README.md +274 -0
  5. package/dist/automated-user.js +19 -0
  6. package/dist/automated-user.js.map +1 -0
  7. package/dist/batcher.js +3 -0
  8. package/dist/batcher.js.map +1 -1
  9. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +613 -53
  10. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -1
  11. package/dist/call-diagnostic/call-diagnostic-metrics.js +240 -103
  12. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
  13. package/dist/call-diagnostic/call-diagnostic-metrics.util.js +5 -0
  14. package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -1
  15. package/dist/call-diagnostic/config.js +19 -4
  16. package/dist/call-diagnostic/config.js.map +1 -1
  17. package/dist/config.js +8 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/index.js +25 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/metrics.js +118 -5
  22. package/dist/metrics.js.map +1 -1
  23. package/dist/metrics.types.js +4 -0
  24. package/dist/metrics.types.js.map +1 -1
  25. package/dist/network-telemetry.js +610 -0
  26. package/dist/network-telemetry.js.map +1 -0
  27. package/dist/new-metrics.js +63 -17
  28. package/dist/new-metrics.js.map +1 -1
  29. package/dist/privacy-and-security-permission-enricher/constants.js +18 -0
  30. package/dist/privacy-and-security-permission-enricher/constants.js.map +1 -0
  31. package/dist/privacy-and-security-permission-enricher/index.js +143 -0
  32. package/dist/privacy-and-security-permission-enricher/index.js.map +1 -0
  33. package/dist/privacy-and-security-permission-enricher/types.js +7 -0
  34. package/dist/privacy-and-security-permission-enricher/types.js.map +1 -0
  35. package/dist/privacy-and-security-permission-enricher/utils.js +71 -0
  36. package/dist/privacy-and-security-permission-enricher/utils.js.map +1 -0
  37. package/dist/types/automated-user.d.ts +2 -0
  38. package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +200 -5
  39. package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +77 -17
  40. package/dist/types/call-diagnostic/config.d.ts +6 -0
  41. package/dist/types/config.d.ts +9 -0
  42. package/dist/types/index.d.ts +5 -3
  43. package/dist/types/metrics.types.d.ts +11 -5
  44. package/dist/types/network-telemetry.d.ts +108 -0
  45. package/dist/types/new-metrics.d.ts +17 -1
  46. package/dist/types/privacy-and-security-permission-enricher/constants.d.ts +6 -0
  47. package/dist/types/privacy-and-security-permission-enricher/index.d.ts +40 -0
  48. package/dist/types/privacy-and-security-permission-enricher/types.d.ts +14 -0
  49. package/dist/types/privacy-and-security-permission-enricher/utils.d.ts +5 -0
  50. package/dist/types/unhandled-exception-telemetry/index.d.ts +60 -0
  51. package/dist/types/unhandled-exception-telemetry/utils.d.ts +6 -0
  52. package/dist/unhandled-exception-telemetry/index.js +331 -0
  53. package/dist/unhandled-exception-telemetry/index.js.map +1 -0
  54. package/dist/unhandled-exception-telemetry/utils.js +105 -0
  55. package/dist/unhandled-exception-telemetry/utils.js.map +1 -0
  56. package/docs/adr/index.md +27 -0
  57. package/docs/architecture.md +437 -0
  58. package/docs/getting-started.md +131 -0
  59. package/docs/index.md +52 -0
  60. package/docs/specs/README.md +88 -0
  61. package/package.json +12 -11
  62. package/schemas/repo-context.schema.json +796 -0
  63. package/src/automated-user.ts +16 -0
  64. package/src/batcher.js +4 -0
  65. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +737 -60
  66. package/src/call-diagnostic/call-diagnostic-metrics.ts +184 -42
  67. package/src/call-diagnostic/call-diagnostic-metrics.util.ts +5 -0
  68. package/src/call-diagnostic/config.ts +16 -0
  69. package/src/call-diagnostic/docs/README.md +535 -0
  70. package/src/config.js +8 -0
  71. package/src/docs/README.md +703 -0
  72. package/src/index.ts +16 -0
  73. package/src/metrics.js +123 -5
  74. package/src/metrics.types.ts +29 -6
  75. package/src/network-telemetry.ts +757 -0
  76. package/src/new-metrics.ts +56 -6
  77. package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
  78. package/src/privacy-and-security-permission-enricher/docs/README.md +356 -0
  79. package/src/privacy-and-security-permission-enricher/index.ts +142 -0
  80. package/src/privacy-and-security-permission-enricher/types.ts +21 -0
  81. package/src/privacy-and-security-permission-enricher/utils.ts +82 -0
  82. package/src/rtcMetrics/docs/README.md +447 -0
  83. package/src/unhandled-exception-telemetry/docs/README.md +536 -0
  84. package/src/unhandled-exception-telemetry/index.ts +404 -0
  85. package/src/unhandled-exception-telemetry/utils.ts +101 -0
  86. package/test/unit/spec/automated-user.ts +43 -0
  87. package/test/unit/spec/batcher.js +43 -0
  88. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +10 -1
  89. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +1336 -131
  90. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +1265 -207
  91. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +27 -0
  92. package/test/unit/spec/metrics.js +14 -5
  93. package/test/unit/spec/network-telemetry.ts +610 -0
  94. package/test/unit/spec/new-metrics.ts +203 -50
  95. package/test/unit/spec/prelogin-metrics-batcher.ts +71 -36
  96. package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
  97. package/test/unit/spec/unhandled-exception-telemetry/utils.ts +109 -0
  98. package/test/unit/spec/unhandled-exception-telemetry.ts +689 -0
@@ -19,17 +19,20 @@ import {
19
19
  ClientEvent,
20
20
  FeatureEvent,
21
21
  EventPayload,
22
- OperationalEvent,
23
22
  MediaQualityEvent,
24
23
  InternalEvent,
25
24
  SubmitClientEventOptions,
26
25
  Table,
27
26
  DelayedClientEvent,
28
27
  DelayedClientFeatureEvent,
28
+ PrivacyAndSecurityPermission,
29
29
  } from './metrics.types';
30
30
  import CallDiagnosticLatencies from './call-diagnostic/call-diagnostic-metrics-latencies';
31
31
  import {setMetricTimings} from './call-diagnostic/call-diagnostic-metrics.util';
32
32
  import {generateCommonErrorMetadata} from './utils';
33
+ import {isAutomatedUser as detectAutomatedUser} from './automated-user';
34
+ import PrivacyAndSecurityPermissionEnricher from './privacy-and-security-permission-enricher';
35
+ import {startUnhandledExceptionTelemetry} from './unhandled-exception-telemetry';
33
36
 
34
37
  /**
35
38
  * Metrics plugin to centralize all types of metrics.
@@ -67,6 +70,8 @@ class Metrics extends WebexPlugin {
67
70
 
68
71
  delayedClientFeatureEventsOverrides: Partial<DelayedClientFeatureEvent['options']> = {};
69
72
 
73
+ private privacyAndSecurityPermissionEnricher: PrivacyAndSecurityPermissionEnricher;
74
+
70
75
  /**
71
76
  * Constructor
72
77
  * @param args
@@ -77,8 +82,19 @@ class Metrics extends WebexPlugin {
77
82
  constructor(...args) {
78
83
  super(...args);
79
84
 
85
+ this.privacyAndSecurityPermissionEnricher = new PrivacyAndSecurityPermissionEnricher(
86
+ (error) => {
87
+ // @ts-ignore
88
+ this.webex.logger.error(
89
+ 'NewMetrics: @submitClientEvent. Privacy and security permission enrichment failed.',
90
+ error
91
+ );
92
+ }
93
+ );
80
94
  // @ts-ignore
81
95
  this.callDiagnosticLatencies = new CallDiagnosticLatencies({}, {parent: this.webex});
96
+ // @ts-ignore
97
+ this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex});
82
98
  this.onReady();
83
99
  }
84
100
 
@@ -88,8 +104,6 @@ class Metrics extends WebexPlugin {
88
104
  private onReady() {
89
105
  // @ts-ignore
90
106
  this.webex.once('ready', () => {
91
- // @ts-ignore
92
- this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex});
93
107
  this.preLoginMetrics = new PreLoginMetrics(
94
108
  // @ts-ignore
95
109
  new PreLoginMetricsBatcher({}, {parent: this.webex}),
@@ -102,6 +116,8 @@ class Metrics extends WebexPlugin {
102
116
  shouldDelay: this.delaySubmitClientEvents,
103
117
  overrides: this.delayedClientEventsOverrides,
104
118
  });
119
+ // @ts-ignore
120
+ startUnhandledExceptionTelemetry(this.webex);
105
121
  });
106
122
  }
107
123
 
@@ -182,6 +198,13 @@ class Metrics extends WebexPlugin {
182
198
  return this.businessMetrics?.isReadyToSubmitEvents() ?? false;
183
199
  }
184
200
 
201
+ /**
202
+ * @returns whether the current user agent belongs to an automated user
203
+ */
204
+ isAutomatedUser() {
205
+ return detectAutomatedUser();
206
+ }
207
+
185
208
  /**
186
209
  * Behavioral event
187
210
  * @param args
@@ -325,7 +348,7 @@ class Metrics extends WebexPlugin {
325
348
  payload?: RecursivePartial<FeatureEvent['payload']>;
326
349
  options: any;
327
350
  }) {
328
- if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) {
351
+ if (!this.isReady) {
329
352
  // @ts-ignore
330
353
  this.webex.logger.log(
331
354
  `NewMetrics: @submitFeatureEvent. Attempted to submit before webex.ready. Event name: ${name}`
@@ -360,7 +383,7 @@ class Metrics extends WebexPlugin {
360
383
  payload?: RecursivePartial<ClientEvent['payload']>;
361
384
  options?: SubmitClientEventOptions;
362
385
  }): Promise<any> {
363
- if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) {
386
+ if (!this.isReady) {
364
387
  // @ts-ignore
365
388
  this.webex.logger.log(
366
389
  `NewMetrics: @submitClientEvent. Attempted to submit before webex.ready. Event name: ${name}`
@@ -373,14 +396,41 @@ class Metrics extends WebexPlugin {
373
396
  options: {meetingId: options?.meetingId},
374
397
  });
375
398
 
376
- return this.callDiagnosticMetrics.submitClientEvent({
399
+ const enrichedPayload = this.privacyAndSecurityPermissionEnricher.enrich({
377
400
  name,
378
401
  payload,
402
+ scope: this.getPermissionScope(options),
403
+ });
404
+
405
+ return this.callDiagnosticMetrics.submitClientEvent({
406
+ name,
407
+ payload: enrichedPayload,
379
408
  options,
380
409
  delaySubmitEvent: this.delaySubmitClientEvents,
381
410
  });
382
411
  }
383
412
 
413
+ /**
414
+ * Stores the latest browser permission state supplied by the client.
415
+ * @param permission latest normalized permission snapshot
416
+ */
417
+ public setPrivacyAndSecurityPermission(permission: PrivacyAndSecurityPermission): void {
418
+ this.privacyAndSecurityPermissionEnricher.setPermission(permission);
419
+ }
420
+
421
+ /**
422
+ * Resolves event options to the call identity used by Call Diagnostic where possible.
423
+ * @param options client event options
424
+ * @returns the permission history scope
425
+ */
426
+ private getPermissionScope(options?: SubmitClientEventOptions): string {
427
+ const meeting = options?.meetingId
428
+ ? (this as any).webex.meetings?.getBasicMeetingInformation?.(options.meetingId)
429
+ : undefined;
430
+
431
+ return options?.correlationId ?? meeting?.correlationId ?? 'default';
432
+ }
433
+
384
434
  /**
385
435
  * Issue request to alias a user's pre-login ID with their CI UUID
386
436
  * @param {string} preLoginId
@@ -0,0 +1,33 @@
1
+ import type {ClientEvent} from '../metrics.types';
2
+ import type {PermissionEnrichmentPolicy} from './types';
3
+
4
+ export const CAMERA_AND_MICROPHONE_PERMISSION_EVENTS = new Set<ClientEvent['name']>([
5
+ 'client.call.initiated',
6
+ 'client.media.capabilities',
7
+ 'client.ice.end',
8
+ 'client.locus.join.request',
9
+ 'client.locus.join.response',
10
+ 'client.media-engine.ready',
11
+ ]);
12
+
13
+ export const MEDIA_TX_PERMISSION_EVENTS = new Set<ClientEvent['name']>([
14
+ 'client.media.tx.start',
15
+ 'client.media.tx.stop',
16
+ ]);
17
+
18
+ export const CONTENT_SHARE_PERMISSION_EVENTS = new Set<ClientEvent['name']>([
19
+ 'client.share.initiated',
20
+ 'client.share.floor-grant.request',
21
+ 'client.share.floor-granted.local',
22
+ ]);
23
+
24
+ export const FINAL_PERMISSION_EVENTS = new Set<ClientEvent['name']>([
25
+ 'client.call.leave',
26
+ 'client.call.remote-ended',
27
+ 'client.call.aborted',
28
+ ]);
29
+
30
+ export const NO_PERMISSION_ENRICHMENT: PermissionEnrichmentPolicy = {
31
+ resources: [],
32
+ terminal: false,
33
+ };
@@ -0,0 +1,356 @@
1
+ ---
2
+ type: Module Spec
3
+ title: 'Privacy and security permission enricher specification'
4
+ description: Responsibilities, boundaries, design, invariants, and verification for the privacy and security permission enricher.
5
+ tags: [module, specification]
6
+ ---
7
+ <!-- sdd-generated-metadata
8
+ doc_kind: module-spec
9
+ generated_from: module-spec@0.3.0
10
+ generated_by: claude-code
11
+ approved_by: rarajes2@cisco.com
12
+ updated_at: 2026-10-07T04:50:31Z
13
+ validation_status: pass
14
+ -->
15
+
16
+ # Privacy and security permission enricher
17
+
18
+ This source-local document at `src/privacy-and-security-permission-enricher/docs/README.md` owns the
19
+ stable specification for **the privacy and security permission enricher**. Ground every claim in
20
+ repository evidence and link to the
21
+ [repository architecture](../../../docs/architecture.md) instead of repeating broader facts.
22
+
23
+ Related context: [documentation index](../../../docs/index.md) ·
24
+ [documentation agent instructions](../../../AGENTS.md)
25
+
26
+ ## Metadata
27
+
28
+ | Field | Value |
29
+ | ------------- | ------------------------------------------------------------ |
30
+ | Owner | Cisco Webex JS SDK — telemetry |
31
+ | Source path | `src/privacy-and-security-permission-enricher/` |
32
+ | Resource kind | module |
33
+ | Status | Active |
34
+ | Last verified | 2026-10-05 at `d6202dd73d` |
35
+ | Module id | `privacy-and-security-permission-enricher` |
36
+ | Parent spec | [`src/docs/README.md`](../../docs/README.md) |
37
+ | Doc kind | Module spec |
38
+ | Coverage score | Pending coverage assessment |
39
+ | Validation status | pass (2026-10-07; validator runtime 01a1147a-c744-7083-9b15-95e91170c79a; 0 blocking, 0 important, 0 medium, 0 minor; conformance and source-fidelity gates pass, no code/spec mismatches found) |
40
+
41
+ ## Applicability
42
+
43
+ Sections preceded by an `Include if` comment are conditional. Retain a
44
+ conditional section only when source evidence or a confirmed developer answer
45
+ satisfies its condition.
46
+
47
+ | Condition ID | Status | Evidence or reason | Owned section |
48
+ | ------------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
49
+ | `module.has_tiers` | N/A | The repository defines no operational or review tier policy for modules | Tier |
50
+ | `module.has_ui` | N/A | The module renders nothing; it transforms an event payload object | UI use-case flow |
51
+ | `module.crosses_service_boundaries` | N/A | No network, event or process boundary is crossed; every operation is a synchronous in-process call | Cross-boundary use-case flow |
52
+ | `module.holds_client_state` | Applicable | `src/privacy-and-security-permission-enricher/index.ts` holds the latest snapshot and a per-scope history map | Client state model |
53
+ | `module.enforces_domain_rules` | Applicable | `src/privacy-and-security-permission-enricher/constants.ts` and `src/privacy-and-security-permission-enricher/index.ts` | Business rules and invariants |
54
+ | `module.is_concurrent_async` | N/A | No promise, timer, subscription or worker; the enrichment call returns synchronously | Concurrency and reactive flow |
55
+ | `module.owns_persistence` | N/A | No store, schema or migration; all state is in-memory and per-instance | Data, schema, and migration |
56
+ | `module.stateful_transitions` | Applicable | Per-scope history is seeded, updated on change and deleted on a terminal event in `src/privacy-and-security-permission-enricher/index.ts` | State machine |
57
+ | `module.exposes_wire_protocol` | N/A | The module adds one field to a payload owned and serialized elsewhere; it performs no serialization | Protocol and wire format |
58
+ | `module.ui_multi_screen` | N/A | No user interface | UI flow |
59
+ | `module.large_data_model` | Applicable only to larger models; N/A here — the model is three permission resources, declared in `src/privacy-and-security-permission-enricher/types.ts` | Data model |
60
+ | `module.returns_caller_errors` | N/A | Body failures are routed to the injected handler and the original payload is returned; only a throwing handler or policy resolution could escape, and the façade's logging handler does not throw | Caller-visible failure modes |
61
+ | `module.module_specific_conventions` | Applicable | The rule table and the projection helpers impose conventions beyond repository-wide rules | Module-specific rules |
62
+ | `module.published_package` | N/A | Not re-exported from `src/index.ts`; reachable only through `src/new-metrics.ts` | Export stability |
63
+ | `module.embedded_in_host` | N/A | The module is not mounted into a host; its parent module owns the plugin registration contract | Host integration and theming |
64
+ | `module.has_design_tradeoff` | Applicable | Change-only reporting with per-scope history, versus reporting the full snapshot on every event | Key design trade-off |
65
+ | `module.has_submodules` | N/A | Derived from the manifest module tree: this module has no child modules | Sub-modules |
66
+
67
+ ## Evidence register
68
+
69
+ | Evidence | What it establishes |
70
+ | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
71
+ | `src/privacy-and-security-permission-enricher/index.ts` | The public surface, the rule table, the enrichment decision order and the per-scope history lifecycle |
72
+ | `src/privacy-and-security-permission-enricher/constants.ts` | The four event groups that are eligible for enrichment and the empty policy used for every other event |
73
+ | `src/privacy-and-security-permission-enricher/types.ts` | The policy, rule and context shapes the module is built around |
74
+ | `src/privacy-and-security-permission-enricher/utils.ts` | Resource resolution from media type, defensive projection and the change diff |
75
+ | `src/new-metrics.ts` | The sole call site: construction with the error handler, the public permission setter and the scope resolution rule |
76
+ | `src/metrics.types.ts` | The externally derived permission and client-event payload types this module operates on |
77
+ | `test/unit/spec/privacy-and-security-permission-enricher.ts` | Behavioral verification of every rule, the change-only reporting rule, scope isolation and the error path |
78
+
79
+ ## Purpose and boundary
80
+
81
+ - Responsibility: deciding which browser permission state is attached to which Call Analyzer client
82
+ event, and attaching only what has changed since the last report within the same call.
83
+ - In scope: the eligible-event rule table; resolving which permission resources a given event cares
84
+ about; projecting the stored snapshot down to those resources; diffing against per-scope history;
85
+ returning either the original payload or an enriched copy; clearing history at the end of a call.
86
+ - Out of scope: obtaining permission state from the browser (the client supplies it through the
87
+ façade), building or submitting the event, and the meaning of the permission fields themselves,
88
+ which is owned by the external Call Analyzer schema.
89
+ - Consumers: `src/new-metrics.ts` only. The class is not re-exported from `src/index.ts`.
90
+
91
+ ## Structure and key files
92
+
93
+ | Path | Responsibility |
94
+ | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | `src/privacy-and-security-permission-enricher/index.ts` | The enricher class, the exported rule table and the policy resolver; authoritative for the enrichment decision order |
96
+ | `src/privacy-and-security-permission-enricher/constants.ts` | The four event-name sets and the no-enrichment policy; authoritative for which events are eligible |
97
+ | `src/privacy-and-security-permission-enricher/types.ts` | The policy, rule and enrichment-context types; authoritative for the rule contract |
98
+ | `src/privacy-and-security-permission-enricher/utils.ts` | Media-type to resource resolution, defensive projection and the change diff; authoritative for what counts as a change |
99
+
100
+ ## Public surface
101
+
102
+ | Surface | Consumer | Compatibility commitment | Source |
103
+ | ---------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
104
+ | `PrivacyAndSecurityPermissionEnricher` constructor | `src/new-metrics.ts` | Takes exactly one argument, the enrichment-error handler. Module-internal; not covered by any published guarantee | `src/privacy-and-security-permission-enricher/index.ts` |
105
+ | `setPermission` | `src/new-metrics.ts` | Stores a defensive projection of the supplied snapshot. Module-internal | `src/privacy-and-security-permission-enricher/index.ts` |
106
+ | `enrich` | `src/new-metrics.ts` | Never mutates its input, and returns the original payload reference when nothing is attached. Errors inside the enrichment body go to the injected handler and the original payload is returned; an error from policy resolution, which runs before the `try`, or from the handler itself propagates to the caller | `src/privacy-and-security-permission-enricher/index.ts` |
107
+ | `PERMISSION_ENRICHMENT_RULES` | Tests, and future rules | Exported for inspection and verification; the array order is the match order | `src/privacy-and-security-permission-enricher/index.ts` |
108
+
109
+ Exact permission field definitions are not restated here. They are derived from the external Call
110
+ Analyzer schema through the types in `src/metrics.types.ts`.
111
+
112
+ ## Dependencies
113
+
114
+ | Dependency | Why it is required | Failure behavior |
115
+ | ------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
116
+ | `src/metrics.types.ts` | Supplies the permission, resource and client-event payload types | A type mismatch is a build-time failure, not a runtime one |
117
+ | `lodash` deep equality | Decides whether a projected resource differs from what was last reported | Any thrown error is caught by the enrichment handler and the original payload is returned |
118
+ | Enrichment-error handler | Supplied by the caller so the module never has to know how to log | The handler is invoked and the original payload is returned; an exception from the handler itself propagates to the caller |
119
+
120
+ ## Requirements
121
+
122
+ | ID | WHAT | WHY | Source evidence | Test or example evidence | Assumptions or gaps | Confidence |
123
+ | --------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------- | ------------------- | ---------- |
124
+ | `MOD-001` | Only events listed in one of the four event sets are enriched; every other event name returns its payload unchanged | Permission state is only meaningful around capture, transmission, share and call-end milestones. Attaching it everywhere would inflate every event | `src/privacy-and-security-permission-enricher/constants.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none | Present |
125
+ | `MOD-002` | Each production event name appears in exactly one rule | Rules are matched by first hit, so an event in two sets would silently take the earlier policy and the later one would be unreachable | `src/privacy-and-security-permission-enricher/index.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none | Present |
126
+ | `MOD-003` | Media transmission and content-share events are scoped to the resource implied by the payload's media type, not to all resources | A microphone-only event must not carry camera state, which would misattribute a permission change to an unrelated action | `src/privacy-and-security-permission-enricher/utils.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none | Present |
127
+ | `MOD-004` | A non-terminal event carries only the resources whose state changed since the last report for the same scope | Repeating unchanged state on every event would make it impossible to tell when a permission actually changed | `src/privacy-and-security-permission-enricher/index.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none | Present |
128
+ | `MOD-005` | A terminal call event carries the full projection of all three resources and does not consult or update history | The end-of-call event is the single record of final permission state, so it must be complete regardless of what was reported earlier | `src/privacy-and-security-permission-enricher/index.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none | Present |
129
+ | `MOD-006` | History is tracked per scope, and a terminal event deletes only its own scope | Concurrent calls must not leak permission history into one another, and one call ending must not reset another call's baseline | `src/privacy-and-security-permission-enricher/index.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none | Present |
130
+ | `MOD-007` | A payload that already carries explicit permission state is returned unchanged; on a non-terminal event that explicit state is merged into the scope's baseline, while on a terminal event it seeds nothing and the scope's history is deleted | An explicitly supplied value is authoritative for that event; adopting it as the baseline prevents the next event from re-reporting the same state, but a terminal event ends the scope, so nothing is kept for it | `src/privacy-and-security-permission-enricher/index.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none | Present |
131
+ | `MOD-008` | Enrichment never mutates the input payload or the stored snapshot; it returns a new object when it attaches anything | The caller reuses the payload it passed in, and the stored snapshot must stay the client-supplied truth | `src/privacy-and-security-permission-enricher/utils.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none | Present |
132
+ | `MOD-009` | No permission is attached before the client has supplied a snapshot, and an unavailable resource is omitted rather than reported empty | Absent state and denied state are different facts; emitting an empty object would be read as a real permission reading | `src/privacy-and-security-permission-enricher/utils.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none | Present |
133
+ | `MOD-010` | A failure inside the enrichment body is handed to the injected handler and the original payload is returned. Two paths are outside that guarantee: policy resolution runs before the try block, and an exception thrown by the injected handler itself propagates to the caller | Telemetry enrichment must not fail a client event submission; from `newMetrics` an escape needs a throwing logger in the façade's handler, or a payload whose fields read by the current resolvers have throwing getters | `src/privacy-and-security-permission-enricher/index.ts`, `src/new-metrics.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | No case covers a throwing handler or a throwing policy resolution | Present |
134
+
135
+ ## Design overview
136
+
137
+ The module is one class plus a declarative rule table, and its whole design is the separation of
138
+ those two things.
139
+
140
+ The rule table in `src/privacy-and-security-permission-enricher/index.ts` pairs each event-name set
141
+ from `src/privacy-and-security-permission-enricher/constants.ts` with a resolver that returns a
142
+ policy: which permission resources this event cares about, and whether the event is terminal. Adding
143
+ or retargeting an event is therefore a change to the sets or the table, not to the enrichment
144
+ algorithm. Rules are matched by first hit, which is why the one-rule-per-event invariant matters and
145
+ why a test asserts it directly.
146
+
147
+ The class owns two pieces of state. `permission` is the latest snapshot the client supplied,
148
+ defensively projected to the three known resources on the way in so an unexpected field cannot reach
149
+ a payload. `lastReported` is a map from scope to the permission state most recently reported for that
150
+ scope; the scope comes from the caller and is the call's correlation identity, which is what makes
151
+ two concurrent calls independent.
152
+
153
+ `enrich` resolves the policy first, outside the try block, because the terminal flag drives the
154
+ `finally` cleanup and must be known even if the body throws. It then applies five decisions in order:
155
+ an explicitly supplied payload wins and, on a non-terminal event, seeds the baseline; an empty resource list means nothing to
156
+ do; a missing snapshot means nothing to report; a terminal event reports the full projection without
157
+ touching history; and otherwise the projection is diffed against history and only the changed
158
+ resources are attached. Because `getChangedPermission` compares whole resource objects by deep
159
+ equality, a change in any field of a resource — not only its status — counts as a change.
160
+
161
+ ## Data flow and sequence coverage
162
+
163
+ The call style is a synchronous in-process method call. There is one operation group — enrich one
164
+ client event — because every public operation shares the same actor, ordering and absence of
165
+ transport or failure propagation. The error branch is shown as an `opt` on the same sequence rather
166
+ than as a separate diagram, since the actor order and the returned value are identical.
167
+
168
+ | Operation group | Entry and outcome | Diagram or evidence | Failure and recovery coverage |
169
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
170
+ | Store the latest snapshot | The client calls the façade setter; the projected snapshot replaces the previous one | `src/privacy-and-security-permission-enricher/index.ts` | No failure path; projection drops unknown resources rather than rejecting the snapshot |
171
+ | Enrich one client event | The façade passes name, payload and scope; the original payload or an enriched copy is returned, and history is updated or cleared | Sequence diagram below | Internal error routed to the handler, original payload returned; terminal cleanup still runs; a throwing handler propagates |
172
+
173
+ ```mermaid
174
+ sequenceDiagram
175
+ participant Facade as newMetrics facade
176
+ participant Enricher as Permission enricher
177
+ participant History as Per-scope history
178
+ Facade->>Enricher: enrich(name, payload, scope)
179
+ Enricher->>Enricher: resolve policy from the rule table
180
+ alt payload already carries permission state
181
+ Enricher->>History: seed baseline unless terminal
182
+ Enricher-->>Facade: original payload
183
+ else no eligible resources or no snapshot stored
184
+ Enricher-->>Facade: original payload
185
+ else terminal call event
186
+ Enricher-->>Facade: payload plus full projection
187
+ else non-terminal eligible event
188
+ Enricher->>History: read last reported state for scope
189
+ Enricher->>Enricher: diff projection against history
190
+ opt nothing changed
191
+ Enricher-->>Facade: original payload
192
+ end
193
+ Enricher->>History: merge changed resources
194
+ Enricher-->>Facade: payload plus changed resources
195
+ end
196
+ opt internal error at any step
197
+ Enricher->>Facade: onEnrichmentError(error)
198
+ Enricher-->>Facade: original payload
199
+ end
200
+ opt policy is terminal
201
+ Enricher->>History: delete this scope
202
+ end
203
+ ```
204
+
205
+ ## Class and component relationships
206
+
207
+ ```mermaid
208
+ classDiagram
209
+ class NewMetrics {
210
+ +setPrivacyAndSecurityPermission()
211
+ +submitClientEvent()
212
+ -getPermissionScope()
213
+ }
214
+ class PrivacyAndSecurityPermissionEnricher {
215
+ -permission
216
+ -lastReported
217
+ -onEnrichmentError
218
+ +setPermission()
219
+ +enrich()
220
+ }
221
+ class PermissionEnrichmentRules {
222
+ +events
223
+ +resolve()
224
+ }
225
+ class PermissionUtils {
226
+ +resolveMediaResources()
227
+ +resolveContentShareResources()
228
+ +projectPrivacyAndSecurityPermission()
229
+ +getChangedPermission()
230
+ }
231
+ NewMetrics --> PrivacyAndSecurityPermissionEnricher : owns one instance
232
+ PrivacyAndSecurityPermissionEnricher --> PermissionEnrichmentRules : matches first hit
233
+ PrivacyAndSecurityPermissionEnricher --> PermissionUtils : projects and diffs
234
+ PermissionEnrichmentRules --> PermissionUtils : resolves resources from media type
235
+ ```
236
+
237
+ ## Use cases and flows
238
+
239
+ | Use case | Actor or caller | Primary steps and outcome | Failure or boundary behavior | Evidence |
240
+ | -------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
241
+ | `UC-001` | First party client | Client supplies a permission snapshot, then a call-initiated event is submitted; camera and microphone state is attached once | A second call-initiated event with unchanged state attaches nothing | `src/privacy-and-security-permission-enricher/index.ts`, `test/unit/spec/privacy-and-security-permission-enricher.ts` |
242
+ | `UC-002` | Media layer, through the façade | An audio transmission event is submitted; only microphone state is attached, and only if it changed | A share media type on a camera-and-microphone event resolves to no resources and attaches nothing | `src/privacy-and-security-permission-enricher/utils.ts`, `test/unit/spec/privacy-and-security-permission-enricher.ts` |
243
+ | `UC-003` | Meetings layer | A call-leave event is submitted; the full three-resource projection is attached and the scope's history is deleted | History for other scopes is untouched, so a concurrent call keeps its baseline | `src/privacy-and-security-permission-enricher/index.ts`, `test/unit/spec/privacy-and-security-permission-enricher.ts` |
244
+ | `UC-004` | Caller with its own state | A payload already containing permission state is submitted; it is returned unchanged and recorded as the new baseline | A terminal event with explicit state clears the scope instead of seeding it | `src/privacy-and-security-permission-enricher/index.ts`, `test/unit/spec/privacy-and-security-permission-enricher.ts` |
245
+ | `UC-005` | Any caller, degraded case | An internal comparison throws; the handler receives the error and the submission proceeds with the original payload | The event is still submitted; only the enrichment is lost | `src/privacy-and-security-permission-enricher/index.ts`, `test/unit/spec/privacy-and-security-permission-enricher.ts` |
246
+
247
+ <!-- Include if: the module holds client-side state. [condition-id: module.holds_client_state] -->
248
+
249
+ ## Client state model
250
+
251
+ | State or slice | Owner | Initial state | Transition triggers | Reset or persistence boundary |
252
+ | --------------------------- | -------------------- | ------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
253
+ | Latest permission snapshot | The enricher instance | Undefined | The façade's public permission setter; stored as a projection of the three resources | Replaced wholesale on every set; never cleared, and never persisted |
254
+ | Per-scope reported history | The enricher instance | Empty map | Merged on every non-terminal enrichment that attaches something, and on an explicit payload | The scope's entry is deleted when a terminal call event is enriched for that scope |
255
+
256
+ <!-- Include if: the module enforces domain rules or entity invariants. [condition-id: module.enforces_domain_rules] -->
257
+
258
+ ## Business rules and invariants
259
+
260
+ | ID | Invariant | WHY | Enforcement source | Test evidence |
261
+ | --------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------- |
262
+ | `INV-001` | An event name belongs to at most one rule set | First-hit matching makes a duplicate silently unreachable | `src/privacy-and-security-permission-enricher/constants.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` |
263
+ | `INV-002` | A projection contains only the requested resources, and is undefined rather than empty when none are present | An empty object in the payload would be read as a real, complete permission reading | `src/privacy-and-security-permission-enricher/utils.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` |
264
+ | `INV-003` | Neither the input payload nor the stored snapshot is mutated | The caller reuses the payload, and the snapshot must remain the client-supplied truth for later diffs | `src/privacy-and-security-permission-enricher/utils.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` |
265
+ | `INV-004` | A terminal event neither reads nor writes history, and always deletes its scope | The final record must be complete and must not depend on, or leave behind, intermediate state | `src/privacy-and-security-permission-enricher/index.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` |
266
+ | `INV-005` | The enrichment body never throws to the caller; failures go to the injected handler, which is expected not to throw | A telemetry enrichment failure must not fail the client event it was decorating | `src/privacy-and-security-permission-enricher/index.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` |
267
+
268
+ <!-- Include if: the module has non-trivial state transitions. [condition-id: module.stateful_transitions] -->
269
+
270
+ ## State machine
271
+
272
+ The state is the per-scope history entry. There is one entry per scope, created on first report and
273
+ destroyed by the terminal event for that scope.
274
+
275
+ ```mermaid
276
+ stateDiagram-v2
277
+ [*] --> NoHistory
278
+ NoHistory --> Reported: eligible event with available permission state
279
+ NoHistory --> NoHistory: ineligible event, no snapshot, or no eligible resources
280
+ NoHistory --> Reported: payload carries explicit non-terminal permission state
281
+ Reported --> Reported: a projected resource differs from the last reported value
282
+ Reported --> Reported: nothing changed, so nothing is attached
283
+ Reported --> [*]: terminal call event deletes this scope
284
+ NoHistory --> [*]: terminal call event deletes this scope
285
+ ```
286
+
287
+ Guards and rejected transitions:
288
+
289
+ - A terminal event never enters the `Reported` write path; it reports the full projection and leaves.
290
+ - An event whose resolved resource list is empty cannot create or advance history, which is what
291
+ keeps a share media type on a camera-and-microphone event from recording a baseline.
292
+ - Deleting a scope affects only that key, so another scope in `Reported` stays there.
293
+
294
+ ## Pitfalls and constraints
295
+
296
+ - Rule order is significant. The rule table is matched by first hit, so moving an event name into an
297
+ earlier set silently changes its policy. The one-rule-per-event test is the guard; keep it passing
298
+ rather than relying on review.
299
+ - The policy must be resolved before the try block. The terminal flag drives the `finally` cleanup,
300
+ so resolving it inside the body would leave a scope uncleaned when the body throws. The cost is that
301
+ a throw during policy resolution is not caught, and neither is a throw from the injected error
302
+ handler: the containment guarantee holds only while that handler does not throw.
303
+ - An explicitly supplied permission payload is authoritative but not free: on a non-terminal event it
304
+ is merged into the baseline, so a caller that supplies state once and then stops will see the next
305
+ automatic report diffed against what it supplied.
306
+ - Change detection is whole-resource, not field-level. Any difference inside a resource object makes
307
+ the whole resource count as changed and be re-reported.
308
+ - The three permission resource names are hard-coded in the setter's projection and in the terminal
309
+ rule. Adding a fourth resource means updating both, not only the type.
310
+
311
+ <!-- Include if: the module has conventions beyond repository-wide rules. [condition-id: module.module_specific_conventions] -->
312
+
313
+ ## Module-specific rules
314
+
315
+ - Do: add or retarget an event by editing the event sets in
316
+ `src/privacy-and-security-permission-enricher/constants.ts` or the rule table in
317
+ `src/privacy-and-security-permission-enricher/index.ts`, and extend the one-rule-per-event test.
318
+ - Do: keep every resolver pure. A resolver receives the payload and returns a policy; it must not
319
+ read or write enricher state.
320
+ - Do not: add a branch to `enrich` for a specific event name. Event-specific behavior belongs in the
321
+ rule table, which is what keeps the algorithm testable.
322
+ - Do not: let an error escape `enrich`. The handler exists so the module never needs to know how the
323
+ host logs, and so a client event is not lost to an enrichment bug. Policy resolution and the handler
324
+ call sit outside the guard today, so a new resolver must not throw and a handler must not throw.
325
+ - Do not: return the stored snapshot object, or any part of it, without projecting first. The
326
+ projection is what prevents a caller from mutating the enricher's state through a payload it holds.
327
+
328
+ <!-- Include if: the module has a non-obvious design trade-off consumers or maintainers must preserve. [condition-id: module.has_design_tradeoff] -->
329
+
330
+ ## Key design trade-off
331
+
332
+ | Chosen trade-off | Preserved invariant or benefit | Cost or limitation | Decision evidence |
333
+ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
334
+ | Report only what changed, keeping per-scope history, instead of attaching the full snapshot to every event | A permission change is visible as an event in the telemetry stream, and payload size stays proportional to real changes rather than to event volume | The enricher must hold mutable per-call state, and a consumer reading a single event in isolation cannot see unchanged resources | `src/privacy-and-security-permission-enricher/index.ts` |
335
+ | Exempt terminal call events from change-only reporting | One event per call always carries the complete final permission state, so a call can be interpreted without replaying every earlier event | That event is larger, and it duplicates state already reported earlier in the same call | `src/privacy-and-security-permission-enricher/index.ts` |
336
+ | Route enrichment-body errors into an injected handler rather than propagating | A client event is not lost because the enrichment body failed, provided the resolver and the handler do not throw | An enrichment bug is silent in the data: the event still arrives, just without permission state, so the handler's log line is the only signal | `src/new-metrics.ts` |
337
+
338
+ ## Verification
339
+
340
+ | Requirement or invariant | Test level | Positive evidence | Negative or boundary evidence | Gap |
341
+ | ------------------------ | ---------- | ------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
342
+ | `MOD-001` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none |
343
+ | `MOD-002` / `INV-001` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none found — the invariant is asserted directly over the rule table rather than by a failing case | none |
344
+ | `MOD-003` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none |
345
+ | `MOD-004` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none |
346
+ | `MOD-005` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none |
347
+ | `MOD-006` / `INV-004` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none |
348
+ | `MOD-007` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none |
349
+ | `MOD-008` / `INV-003` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none found — mutation is asserted absent rather than provoked | none |
350
+ | `MOD-009` / `INV-002` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none |
351
+ | `MOD-010` / `INV-005` | Unit | `test/unit/spec/privacy-and-security-permission-enricher.ts` | `test/unit/spec/privacy-and-security-permission-enricher.ts` | none |
352
+ | Scope resolution from event options | Unit | `test/unit/spec/new-metrics.ts` | none found | The fallback chain from correlation identifier to meeting correlation identifier to the default scope is exercised through the façade, not by a dedicated case |
353
+
354
+ Record coverage gaps explicitly and link follow-up work. A module specification
355
+ is complete only when its public surface, invariants, failure modes, and test
356
+ evidence agree with the implementation.