Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,8 @@ const CONST = {
CONTEXT_FULLSTORY: 'Fullstory',
CONTEXT_POLICIES: 'Policies',
TAG_ACTIVE_POLICY: 'active_policy_id',
TAG_POLICIES_COUNT: 'policies_count',
TAG_REPORTS_COUNT: 'reports_count',
TAG_NUDGE_MIGRATION_COHORT: 'nudge_migration_cohort',
TAG_AUTHENTICATION_FUNCTION: 'authentication_function',
TAG_AUTHENTICATION_ERROR_TYPE: 'authentication_error_type',
Expand Down
71 changes: 71 additions & 0 deletions src/libs/telemetry/TelemetrySynchronizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ Onyx.connectWithoutView({
},
});

Onyx.connectWithoutView({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
if (!value) {
return;
}
sendReportsCountTag(Object.keys(value).length);
Copy link
Contributor

Choose a reason for hiding this comment

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

❌ PERF-11 (docs)

This Onyx connection subscribes to the entire REPORT collection, causing the callback to re-execute whenever ANY report changes in ANY way (e.g., a message is added, status changes, metadata updates). Since the callback only needs the COUNT of reports, this creates unnecessary re-renders and performance overhead.

Suggested fix: Consider implementing a more optimized approach:

  1. Track report count separately in Onyx (e.g., as a separate key like REPORT_COUNT)
  2. Or use a selector that only returns the count/keys to minimize data processing
  3. Or debounce the tag update to avoid excessive Sentry API calls

Example with selector (though this still triggers on any report change):

Onyx.connectWithoutView({
    key: ONYXKEYS.COLLECTION.REPORT,
    waitForCollectionCallback: true,
    selector: (reports) => Object.keys(reports ?? {}).length,
    callback: (count) => {
        if (!count) {
            return;
        }
        sendReportsCountTag(count);
    },
});

However, the ideal solution would be tracking report count separately to avoid re-computation on every report change.


Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is right in fact that we connect to large dataset.

This altough is not used in any screen component so comment is wrong about rerenders overhead.

Onyx.connectWithoutView does not have selector prop

},
});

Onyx.connectWithoutView({
key: ONYXKEYS.NVP_TRY_NEW_DOT,
callback: (value) => {
Expand All @@ -61,12 +72,67 @@ Onyx.connectWithoutView({
},
});

/**
* Buckets policy count into cohorts for Sentry tagging
*/
function bucketPolicyCount(count: number): string {
if (count <= 1) {
return '0-1';
}
if (count <= 10) {
return '2-10';
}
if (count <= 50) {
return '11-50';
}
if (count <= 100) {
return '51-100';
}
if (count <= 250) {
return '101-250';
}
if (count <= 500) {
return '251-500';
}
if (count <= 1000) {
return '501-1000';
}
return '1000+';
}

/**
* Buckets report count into cohorts for Sentry tagging
*/
function bucketReportCount(count: number): string {
if (count <= 60) {
return '0-60';
}
if (count <= 300) {
return '61-300';
}
if (count <= 1000) {
return '301-1000';
}
if (count <= 2500) {
return '1001-2500';
}
if (count <= 5000) {
return '2501-5000';
}
if (count <= 10000) {
return '5001-10000';
}
return '10000+';
}

function sendPoliciesContext() {
if (!policies || !session?.email || !activePolicyID) {
return;
}
const activePolicies = getActivePolicies(policies, session.email).map((policy) => policy.id);
const policiesCountBucket = bucketPolicyCount(activePolicies.length);
Sentry.setTag(CONST.TELEMETRY.TAG_ACTIVE_POLICY, activePolicyID);
Sentry.setTag(CONST.TELEMETRY.TAG_POLICIES_COUNT, policiesCountBucket);
Sentry.setContext(CONST.TELEMETRY.CONTEXT_POLICIES, {activePolicyID, activePolicies});
}

Expand All @@ -77,3 +143,8 @@ function sendTryNewDotCohortTag() {
}
Sentry.setTag(CONST.TELEMETRY.TAG_NUDGE_MIGRATION_COHORT, cohort);
}

function sendReportsCountTag(reportsCount: number) {
const reportsCountBucket = bucketReportCount(reportsCount);
Sentry.setTag(CONST.TELEMETRY.TAG_REPORTS_COUNT, reportsCountBucket);
}
41 changes: 41 additions & 0 deletions src/libs/telemetry/middlewares/copyTagsToChildSpans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import CONST from '@src/CONST';
import type {TelemetryBeforeSend} from './index';

/**
* List of tags that should be copied from the transaction to all child spans
*/
const TAGS_TO_COPY = [CONST.TELEMETRY.TAG_POLICIES_COUNT, CONST.TELEMETRY.TAG_REPORTS_COUNT, CONST.TELEMETRY.TAG_ACTIVE_POLICY, CONST.TELEMETRY.TAG_NUDGE_MIGRATION_COHORT] as const;

/**
* Middleware that copies specific tags from the transaction event to all child spans.
* This ensures that child spans inherit important context from the parent transaction.
*/
const copyTagsToChildSpans: TelemetryBeforeSend = (event) => {
if (!event.spans || event.spans.length === 0) {
return event;
}

if (!event.tags) {
return event;
}

const spans = event.spans.map((span) => {
const updatedTags: Record<string, unknown> = {};

for (const tagKey of TAGS_TO_COPY) {
const tagValue = event.tags?.[tagKey];
if (tagValue !== undefined) {
updatedTags[tagKey] = tagValue;
}
}

return {
...span,
tags: updatedTags,
};
});

return {...event, spans};
};

export default copyTagsToChildSpans;
3 changes: 2 additions & 1 deletion src/libs/telemetry/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type {EventHint, TransactionEvent} from '@sentry/core';
import copyTagsToChildSpans from './copyTagsToChildSpans';
import emailDomainFilter from './emailDomainFilter';
import firebasePerformanceFilter from './firebasePerformanceFilter';
import minDurationFilter from './minDurationFilter';

type TelemetryBeforeSend = (event: TransactionEvent, hint: EventHint) => TransactionEvent | null | Promise<TransactionEvent | null>;

const middlewares: TelemetryBeforeSend[] = [emailDomainFilter, firebasePerformanceFilter, minDurationFilter];
const middlewares: TelemetryBeforeSend[] = [emailDomainFilter, firebasePerformanceFilter, minDurationFilter, copyTagsToChildSpans];

function processBeforeSendTransactions(event: TransactionEvent, hint: EventHint): Promise<TransactionEvent | null> {
return middlewares.reduce(
Expand Down
Loading