From cd11afa297ad1fa227b75e311e5cb35050bc7837 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 6 Mar 2026 11:46:19 +0100 Subject: [PATCH 1/7] add eslint rule enforcing getReportName being read-only --- eslint.config.mjs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 361e2b63b90ae..ba2e0f0fbea33 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -592,6 +592,42 @@ const config = defineConfig([ }, }, + { + files: ['src/libs/ReportNameUtils.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + // Global selectors (must be repeated since file-scoped rules override the global ones) + { + selector: 'TSEnumDeclaration', + message: "Please don't declare enums, use union types instead.", + }, + { + selector: 'CallExpression[callee.object.name="React"][callee.property.name="forwardRef"]', + message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs', + }, + { + selector: 'CallExpression[callee.name="getUrlWithBackToParam"]', + message: + 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', + }, + { + selector: 'LabeledStatement', + message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize. It is also deprecated.', + }, + // File-specific: getReportName must only read from reportAttributesDerivedValue — no function calls allowed. + { + selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', + message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', + }, + ], + }, + }, + { files: ['src/**/*'], ignores: ['src/languages/**', 'src/CONST/index.ts', 'src/NAICS.ts'], From 86d90b79652838d8979521035a38223b5e7f8f68 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 6 Mar 2026 12:13:04 +0100 Subject: [PATCH 2/7] add eslint rule enforcing getReportName being read-only --- eslint.changed.config.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/eslint.changed.config.mjs b/eslint.changed.config.mjs index 79114b59a65eb..eedbdc9ab556a 100644 --- a/eslint.changed.config.mjs +++ b/eslint.changed.config.mjs @@ -106,6 +106,19 @@ const config = defineConfig([ ], }, }, + + { + files: ['src/libs/ReportNameUtils.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', + message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', + }, + ], + }, + }, ]); export default config; From ecbed280068bcfa182086fa02d33e9e85d3f7f8d Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 9 Mar 2026 11:14:00 +0100 Subject: [PATCH 3/7] remove part from lint-changed --- eslint.changed.config.mjs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/eslint.changed.config.mjs b/eslint.changed.config.mjs index eedbdc9ab556a..79114b59a65eb 100644 --- a/eslint.changed.config.mjs +++ b/eslint.changed.config.mjs @@ -106,19 +106,6 @@ const config = defineConfig([ ], }, }, - - { - files: ['src/libs/ReportNameUtils.ts'], - rules: { - 'no-restricted-syntax': [ - 'error', - { - selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', - message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', - }, - ], - }, - }, ]); export default config; From 56840bf9203d7bdc64ada8847b430efbabc8b19a Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 9 Mar 2026 11:23:57 +0100 Subject: [PATCH 4/7] The repetition pattern for ReportNameUtils --- eslint.changed.config.mjs | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/eslint.changed.config.mjs b/eslint.changed.config.mjs index 79114b59a65eb..8ca8864f1e775 100644 --- a/eslint.changed.config.mjs +++ b/eslint.changed.config.mjs @@ -106,6 +106,66 @@ const config = defineConfig([ ], }, }, + + { + files: ['src/libs/ReportNameUtils.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + // All selectors from parent blocks must be repeated since file-scoped rules override them + // From mainConfig's global block + { + selector: 'TSEnumDeclaration', + message: "Please don't declare enums, use union types instead.", + }, + { + selector: 'CallExpression[callee.object.name="React"][callee.property.name="forwardRef"]', + message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs', + }, + { + selector: 'CallExpression[callee.name="getUrlWithBackToParam"]', + message: + 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', + }, + { + selector: 'LabeledStatement', + message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize. It is also deprecated.', + }, + // From the **/*.ts, **/*.tsx block above + { + selector: 'ImportNamespaceSpecifier[parent.source.value=/^@libs/]', + message: 'Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"', + }, + { + selector: 'ImportNamespaceSpecifier[parent.source.value=/^@userActions/]', + message: 'Namespace imports from @userActions are not allowed. Use named imports instead. Example: import { action } from "@userActions/module"', + }, + { + selector: + 'JSXElement[openingElement.name.name=/^Pressable(WithoutFeedback|WithFeedback|WithDelayToggle|WithoutFocus)$/]:not(:has(JSXAttribute[name.name="sentryLabel"]))', + message: 'All Pressable components must include sentryLabel prop for Sentry tracking. Example: ', + }, + // From the **/libs/**/*.{ts,tsx} block above + { + selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\.\\./]', + message: 'Namespace imports are not allowed. Use named imports instead. Example: import { method } from "../libs/module"', + }, + { + selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\./]', + message: 'Namespace imports are not allowed. Use named imports instead. Example: import { method } from "./libs/module"', + }, + // File-specific: getReportName must only read from reportAttributesDerivedValue — no function calls allowed. + { + selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', + message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', + }, + ], + }, + }, ]); export default config; From cb6813b3ac36f1be54fb2489c110fe14300d40d3 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 11 Mar 2026 10:34:54 +0100 Subject: [PATCH 5/7] eslint custom rule --- eslint-plugin-report-name-utils/index.mjs | 30 ++++++++++++ eslint.changed.config.mjs | 59 ++--------------------- eslint.config.mjs | 35 ++------------ 3 files changed, 36 insertions(+), 88 deletions(-) create mode 100644 eslint-plugin-report-name-utils/index.mjs diff --git a/eslint-plugin-report-name-utils/index.mjs b/eslint-plugin-report-name-utils/index.mjs new file mode 100644 index 0000000000000..0c4d8cfc2860f --- /dev/null +++ b/eslint-plugin-report-name-utils/index.mjs @@ -0,0 +1,30 @@ +/** + * ESLint plugin that enforces architectural constraints on ReportNameUtils.ts. + * + * `getReportName` must remain a pure read-only function — it reads from + * pre-computed `reportAttributesDerivedValue` and must never call other + * functions. All computation belongs in `computeReportName`. + */ + +const noFunctionCallInGetReportName = { + meta: { + type: 'problem', + docs: {description: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.'}, + messages: {noFunctionCall: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.'}, + schema: [], + }, + create(context) { + return { + 'FunctionDeclaration[id.name="getReportName"] CallExpression'(node) { + context.report({node, messageId: 'noFunctionCall'}); + }, + }; + }, +}; + +export default { + meta: {name: 'eslint-plugin-report-name-utils'}, + rules: { + 'no-function-call-in-get-report-name': noFunctionCallInGetReportName, + }, +}; diff --git a/eslint.changed.config.mjs b/eslint.changed.config.mjs index 8ca8864f1e775..87417ae72a2ab 100644 --- a/eslint.changed.config.mjs +++ b/eslint.changed.config.mjs @@ -1,4 +1,5 @@ import {defineConfig} from 'eslint/config'; +import reportNameUtilsPlugin from './eslint-plugin-report-name-utils/index.mjs'; import mainConfig from './eslint.config.mjs'; const restrictedIconImportPaths = [ @@ -109,62 +110,8 @@ const config = defineConfig([ { files: ['src/libs/ReportNameUtils.ts'], - rules: { - 'no-restricted-syntax': [ - 'error', - // All selectors from parent blocks must be repeated since file-scoped rules override them - // From mainConfig's global block - { - selector: 'TSEnumDeclaration', - message: "Please don't declare enums, use union types instead.", - }, - { - selector: 'CallExpression[callee.object.name="React"][callee.property.name="forwardRef"]', - message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs', - }, - { - selector: 'CallExpression[callee.name="getUrlWithBackToParam"]', - message: - 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', - }, - { - selector: 'LabeledStatement', - message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', - }, - { - selector: 'WithStatement', - message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize. It is also deprecated.', - }, - // From the **/*.ts, **/*.tsx block above - { - selector: 'ImportNamespaceSpecifier[parent.source.value=/^@libs/]', - message: 'Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"', - }, - { - selector: 'ImportNamespaceSpecifier[parent.source.value=/^@userActions/]', - message: 'Namespace imports from @userActions are not allowed. Use named imports instead. Example: import { action } from "@userActions/module"', - }, - { - selector: - 'JSXElement[openingElement.name.name=/^Pressable(WithoutFeedback|WithFeedback|WithDelayToggle|WithoutFocus)$/]:not(:has(JSXAttribute[name.name="sentryLabel"]))', - message: 'All Pressable components must include sentryLabel prop for Sentry tracking. Example: ', - }, - // From the **/libs/**/*.{ts,tsx} block above - { - selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\.\\./]', - message: 'Namespace imports are not allowed. Use named imports instead. Example: import { method } from "../libs/module"', - }, - { - selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\./]', - message: 'Namespace imports are not allowed. Use named imports instead. Example: import { method } from "./libs/module"', - }, - // File-specific: getReportName must only read from reportAttributesDerivedValue — no function calls allowed. - { - selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', - message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', - }, - ], - }, + plugins: {'report-name-utils': reportNameUtilsPlugin}, + rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'}, }, ]); diff --git a/eslint.config.mjs b/eslint.config.mjs index ba2e0f0fbea33..6051af243bcde 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,7 @@ import path from 'node:path'; import {fileURLToPath} from 'node:url'; import typescriptEslint from 'typescript-eslint'; import reactCompilerCompat from './eslint-plugin-react-compiler-compat/index.mjs'; +import reportNameUtilsPlugin from './eslint-plugin-report-name-utils/index.mjs'; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -594,38 +595,8 @@ const config = defineConfig([ { files: ['src/libs/ReportNameUtils.ts'], - rules: { - 'no-restricted-syntax': [ - 'error', - // Global selectors (must be repeated since file-scoped rules override the global ones) - { - selector: 'TSEnumDeclaration', - message: "Please don't declare enums, use union types instead.", - }, - { - selector: 'CallExpression[callee.object.name="React"][callee.property.name="forwardRef"]', - message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs', - }, - { - selector: 'CallExpression[callee.name="getUrlWithBackToParam"]', - message: - 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', - }, - { - selector: 'LabeledStatement', - message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', - }, - { - selector: 'WithStatement', - message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize. It is also deprecated.', - }, - // File-specific: getReportName must only read from reportAttributesDerivedValue — no function calls allowed. - { - selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', - message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', - }, - ], - }, + plugins: {'report-name-utils': reportNameUtilsPlugin}, + rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'}, }, { From 68b2717dab1731bd1ed0aad88003fc99dbe5a7ee Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 16 Mar 2026 11:20:57 +0100 Subject: [PATCH 6/7] sentry debug spans to be displayed only locally --- src/libs/telemetry/activeSpans.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libs/telemetry/activeSpans.ts b/src/libs/telemetry/activeSpans.ts index 84af93aa09fcd..1a1e0387d316c 100644 --- a/src/libs/telemetry/activeSpans.ts +++ b/src/libs/telemetry/activeSpans.ts @@ -1,7 +1,6 @@ import type {SpanAttributeValue, StartSpanOptions} from '@sentry/core'; import * as Sentry from '@sentry/react-native'; import {AppState} from 'react-native'; -import Log from '@libs/Log'; import CONST from '@src/CONST'; type ActiveSpanEntry = { @@ -25,7 +24,7 @@ function startSpan(spanId: string, options: StartSpanOptions, extraOptions: Star } // End any existing span for this name cancelSpan(spanId); - Log.info(`[Sentry][${spanId}] Starting span`, undefined, { + console.debug(`[Sentry][${spanId}] Starting span`, { spanId, spanOptions: options, spanExtraOptions: extraOptions, @@ -45,13 +44,13 @@ function endSpan(spanId: string) { const entry = activeSpans.get(spanId); if (!entry) { - Log.info(`[Sentry][${spanId}] Trying to end span but it does not exist`, undefined, {spanId, timestamp: Date.now()}); + console.debug(`[Sentry][${spanId}] Trying to end span but it does not exist`, {spanId, timestamp: Date.now()}); return; } const {span, startTime} = entry; const now = performance.now(); const durationMs = Math.round(now - startTime); - Log.info(`[Sentry][${spanId}] Ending span (${durationMs}ms)`, undefined, {spanId, durationMs, timestamp: now}); + console.debug(`[Sentry][${spanId}] Ending span (${durationMs}ms)`, {spanId, durationMs, timestamp: now}); span.setStatus({code: 1}); span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_FINISHED_MANUALLY, true); span.end(); @@ -63,7 +62,7 @@ function cancelSpan(spanId: string) { if (!entry) { return; } - Log.info(`[Sentry][${spanId}] Canceling span`, undefined, {spanId, timestamp: Date.now()}); + console.debug(`[Sentry][${spanId}] Canceling span`, {spanId, timestamp: Date.now()}); entry.span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_CANCELED, true); // In Sentry there are only OK or ERROR status codes. // We treat canceled spans as OK, so we can properly track spans that are not finished at all (their status would be different) From c85634f86e6228f5ca94199494457d897207b141 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 16 Mar 2026 11:28:40 +0100 Subject: [PATCH 7/7] remove tryEndSpan as it's purpose is no more needed. No `/api/Log` for Sentry spans are sent --- src/libs/telemetry/activeSpans.ts | 15 +-------------- .../inbox/report/comment/TextCommentFragment.tsx | 4 ++-- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/libs/telemetry/activeSpans.ts b/src/libs/telemetry/activeSpans.ts index 1a1e0387d316c..7bf4e47a6942e 100644 --- a/src/libs/telemetry/activeSpans.ts +++ b/src/libs/telemetry/activeSpans.ts @@ -84,19 +84,6 @@ function cancelSpansByPrefix(prefix: string) { } } -/** - * Ends a span only if it's currently active. Unlike `endSpan`, this silently no-ops - * when the span doesn't exist, making it safe for render paths where the span - * may or may not have been started. - */ -function tryEndSpan(spanId: string): boolean { - if (!activeSpans.has(spanId)) { - return false; - } - endSpan(spanId); - return true; -} - function getSpan(spanId: string) { return activeSpans.get(spanId)?.span; } @@ -107,4 +94,4 @@ function endSpanWithAttributes(spanId: string, attributes: Record tags and emoji tag