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 79114b59a65eb..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 = [ @@ -106,6 +107,12 @@ const config = defineConfig([ ], }, }, + + { + files: ['src/libs/ReportNameUtils.ts'], + plugins: {'report-name-utils': reportNameUtilsPlugin}, + rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'}, + }, ]); export default config; diff --git a/eslint.config.mjs b/eslint.config.mjs index 361e2b63b90ae..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); @@ -592,6 +593,12 @@ const config = defineConfig([ }, }, + { + files: ['src/libs/ReportNameUtils.ts'], + plugins: {'report-name-utils': reportNameUtilsPlugin}, + rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'}, + }, + { files: ['src/**/*'], ignores: ['src/languages/**', 'src/CONST/index.ts', 'src/NAICS.ts'], diff --git a/src/libs/telemetry/activeSpans.ts b/src/libs/telemetry/activeSpans.ts index 84af93aa09fcd..7bf4e47a6942e 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) @@ -85,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; } @@ -108,4 +94,4 @@ function endSpanWithAttributes(spanId: string, attributes: Record tags and emoji tag