Skip to content

feat: add device permission diagnostics metrics - #5155

Closed
gabrielchl wants to merge 2 commits into
webex:nextfrom
gabrielchl:gabrilee/device-permission-diagnostics-metrics
Closed

feat: add device permission diagnostics metrics#5155
gabrielchl wants to merge 2 commits into
webex:nextfrom
gabrielchl:gabrilee/device-permission-diagnostics-metrics

Conversation

@gabrielchl

Copy link
Copy Markdown
Contributor

COMPLETES https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-842561

This pull request addresses

the event definitions were added here: https://sqbu-github.cisco.com/WebExSquared/event-dictionary/pull/2520

by making the following changes

adding metrics to applicable events as defined in https://confluence-eng-gpk2.cisco.com/conf/spaces/CAL/pages/866309853/Daily+Meeting+Issue+Report+Initiative

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tooling change
  • Internal code refactor

The following scenarios were tested

< ENUMERATE TESTS PERFORMED, WHETHER MANUAL OR AUTOMATED >

The GAI Coding Policy And Copyright Annotation Best Practices

  • GAI was not used (or, no additional notation is required)
  • Code was generated entirely by GAI
  • GAI was used to create a draft that was subsequently customized or modified
  • Coder created a draft manually that was non-substantively modified by GAI (e.g., refactoring was performed by GAI on manually written code)
  • Tool used for AI assistance (GitHub Copilot / Other - specify)
    • Github Copilot
    • Other - Please Specify
  • This PR is related to
    • Feature
    • Defect fix
    • Tech Debt
    • Automation

I certified that

  • I have read and followed contributing guidelines
  • I discussed changes with code owners prior to submitting this pull request
  • I have not skipped any automated checks
  • All existing and new tests passed
  • I have updated the documentation accordingly

Make sure to have followed the contributing guidelines before submitting.

@gabrielchl
gabrielchl requested review from a team as code owners August 7, 2026 14:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8285190d3b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +559 to +560
if (!projectedPermission) {
return payload;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear permission scope when final snapshot is unavailable

When a registered provider returns undefined (or no projected resources) for a terminal event such as client.call.leave, this early return skips the final-event cleanup just below it. If the same permission scope is reused later, the stale lastReportedPrivacyAndSecurityPermission entry makes the next call look unchanged and suppresses the initial permission metrics, and it also leaves one map entry behind for each affected completed meeting. Move the isFinalEvent delete before this return path as well.

Useful? React with 👍 / 👎.

@aws-amplify-us-east-2

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-5155.d3m3l2kee0btzx.amplifyapp.com

@chrisadubois chrisadubois left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall looks good but just because I raised this design question as a thought, I asked an agent to make a draft PR that would demonstrate some of those changes, take them or leave them. PR is approved as is since it accomplishes the goal: gabrielchl#1

import {generateCommonErrorMetadata} from './utils';
import {isAutomatedUser as detectAutomatedUser} from './automated-user';

const CAMERA_AND_MICROPHONE_PERMISSION_EVENTS = new Set<ClientEvent['name']>([

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: maybe we could define these constants elsewhere rather than inline in this file. It'd be nice to be able to generate the values they need, but probably more complexity than is needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i think it's probably best to somehow be able to import the type from event dictionary (not necessarily a const but at least it'll give us type checking) but we should prob do that separately

'client.call.aborted',
]);

type PermissionResource = keyof PrivacyAndSecurityPermission;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick these should be defined in the types file

type PermissionResource = keyof PrivacyAndSecurityPermission;
type PermissionState = PrivacyAndSecurityPermission[PermissionResource];

const DEFAULT_PERMISSION_SCOPE = 'default';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought, do we really need this? couldn't we just default the parameter? can we store this in some constants file elsewhere?


const DEFAULT_PERMISSION_SCOPE = 'default';

const isSamePermissionState = (current?: PermissionState, previous?: PermissionState): boolean =>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: isEqual ... hehe

const isSamePermissionState = (current?: PermissionState, previous?: PermissionState): boolean =>
current?.status === previous?.status && current?.reason === previous?.reason;

const getPermissionResourcesForEvent = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: this code kinda reminds me of the strategy pattern, or chain of responsibility type.

type Payload = RecursivePartial<ClientEvent['payload']>;

type PermissionEnrichmentPolicy = {
  resources: readonly PermissionResource[];
  terminal: boolean;
};

type PermissionEnrichmentRule = {
  events: ReadonlySet<ClientEvent['name']>;
  resolve: (payload?: Payload) => PermissionEnrichmentPolicy;
};

const mediaResources = (payload?: Payload): PermissionResource[] => {
  switch (payload?.mediaType) {
    case 'audio':
      return ['microphone'];
    case 'video':
      return ['camera'];
    case 'share':
      return ['contentShare'];
    default:
      return [];
  }
};

const PERMISSION_ENRICHMENT_RULES: readonly PermissionEnrichmentRule[] = [
  {
    events: CAMERA_AND_MICROPHONE_PERMISSION_EVENTS,
    resolve: () => ({
      resources: ['camera', 'microphone'],
      terminal: false,
    }),
  },
  {
    events: MEDIA_TX_PERMISSION_EVENTS,
    resolve: (payload) => ({
      resources: mediaResources(payload),
      terminal: false,
    }),
  },
  {
    events: CONTENT_SHARE_PERMISSION_EVENTS,
    resolve: (payload) => ({
      resources: payload?.mediaType === 'share' ? ['contentShare'] : [],
      terminal: false,
    }),
  },
  {
    events: FINAL_PERMISSION_EVENTS,
    resolve: () => ({
      resources: ['camera', 'microphone', 'contentShare'],
      terminal: true,
    }),
  },
];

const NO_PERMISSION_ENRICHMENT: PermissionEnrichmentPolicy = {
  resources: [],
  terminal: false,
};

const resolvePermissionEnrichmentPolicy = (
  name: ClientEvent['name'],
  payload?: Payload
): PermissionEnrichmentPolicy =>
  PERMISSION_ENRICHMENT_RULES.find(({events}) => events.has(name))?.resolve(payload) ??
  NO_PERMISSION_ENRICHMENT;
const {resources, terminal: isFinalEvent} =
  resolvePermissionEnrichmentPolicy(name, payload);

Something like this maybe? (Ai generated) but I think it formalizes the idea a bit better. To me it reads nicer.

this is, of course, your decision, this code functions perfectly fine as is.

return [];
};

const projectPrivacyAndSecurityPermission = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then this becomes:

const policy = resolvePermissionEnrichmentPolicy(name, payload);
const permission = this.privacyAndSecurityPermissionProvider?.();

const projectedPermission = permission
  ? projectPrivacyAndSecurityPermission(permission, policy.resources)
  : undefined;

* @param args client event name and payload
* @returns the original or enriched payload
*/
private addPrivacyAndSecurityPermission({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then

class PrivacyAndSecurityPermissionEnricher {
  private lastReported = new Map<string, PrivacyAndSecurityPermission>();
  private provider?: PrivacyAndSecurityPermissionProvider;

  enrich({name, payload, scope}: EnrichmentContext): Payload | undefined {
    const policy = resolvePermissionEnrichmentPolicy(name, payload);

    try {
      // Preserve explicit permissions and record their baseline.
      // Obtain the provider snapshot.
      // Project the resources selected by the policy.
      // Emit only changes, or the complete terminal snapshot.
      return enrichedPayload;
    } catch (error) {
      return payload;
    } finally {
      if (policy.terminal) {
        this.lastReported.delete(scope);
      }
    }
  }
}

const provider = sinon.stub().returns(permission);
const payload = {mediaType: 'audio' as const};

webex.internal.newMetrics.setPrivacyAndSecurityPermissionProvider(provider);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really get the provider bit. why was this done?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the privacyAndSecurityPermissionProvider is a function that will be provided by cantina, returning the permission statuses on call, it's how the SDK can "ask" the client for the permission statuses


assert.deepEqual(submittedPayload, {
mediaType: 'audio',
privacyAndSecurityPermission: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97a54bda89

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return payload;
}

this.lastReported.set(scope, {...lastReported, ...changedPermission});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep permission history tied to sent events

When a changed permission is observed on an event that Call Diagnostics later drops, this line still records it as reported. For example, CallDiagnosticMetrics.shouldSendEvent() limits duplicate client.media.tx.start events per media type/correlation; if the microphone permission changes on a duplicate tx-start, that metric is discarded after enrichment, but lastReported is already advanced, so a later sendable client.media.tx.stop in the same scope omits the changed permission snapshot. Update the history only after the metric is actually queued/sent, or avoid enriching events that will be suppressed.

Useful? React with 👍 / 👎.

Comment on lines +396 to +400
const enrichedPayload = this.privacyAndSecurityPermissionEnricher.enrich({
name,
payload,
scope: this.getPermissionScope(options),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enrich fetch-built client events

This enrichment only runs through submitClientEvent, but the public buildClientEventFetchRequestOptions path below still forwards the original payload directly to Call Diagnostics. In the documented before-unload flow for events such as client.call.leave, callers use the fetch-options builder instead of submitClientEvent, so the provider is never invoked, the final permission snapshot is missing, and the scope is not cleared if no normal terminal event is submitted. Apply the same enrichment/scope logic before building fetch options.

Useful? React with 👍 / 👎.

@gabrielchl
gabrielchl marked this pull request as draft August 13, 2026 09:49
@gabrielchl gabrielchl added the validated If the pull request is validated for automation. label Aug 13, 2026
@gabrielchl

Copy link
Copy Markdown
Contributor Author

closing in favour of #5169

@gabrielchl gabrielchl closed this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

validated If the pull request is validated for automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants