Skip to content
30 changes: 30 additions & 0 deletions eslint-plugin-report-name-utils/index.mjs
Original file line number Diff line number Diff line change
@@ -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) {

Check failure on line 18 in eslint-plugin-report-name-utils/index.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Expected longform method syntax for string literal keys

Check failure on line 18 in eslint-plugin-report-name-utils/index.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Expected longform method syntax for string literal keys
context.report({node, messageId: 'noFunctionCall'});
},
};
},
};

export default {
meta: {name: 'eslint-plugin-report-name-utils'},
rules: {
'no-function-call-in-get-report-name': noFunctionCallInGetReportName,
},
};
7 changes: 7 additions & 0 deletions eslint.changed.config.mjs
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -106,6 +107,12 @@ const config = defineConfig([
],
},
},

{
files: ['src/libs/ReportNameUtils.ts'],
Copy link
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-3 (docs)

The eslint.changed.config.mjs file already imports and spreads the entire mainConfig from eslint.config.mjs via ...mainConfig at line 39. Since the report-name-utils plugin rule block was added to eslint.config.mjs, it is automatically inherited here. Adding the same block again in eslint.changed.config.mjs is redundant duplication — the import of reportNameUtilsPlugin on line 2 and the rule block (lines 111–115) should be removed from this file.


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

plugins: {'report-name-utils': reportNameUtilsPlugin},
rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'},
},
]);

export default config;
7 changes: 7 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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'],
Expand Down
24 changes: 5 additions & 19 deletions src/libs/telemetry/activeSpans.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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,
Expand All @@ -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();
Expand All @@ -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)
Expand All @@ -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;
}
Expand All @@ -108,4 +94,4 @@ function endSpanWithAttributes(spanId: string, attributes: Record<string, SpanAt
endSpan(spanId);
}

export {startSpan, endSpan, tryEndSpan, endSpanWithAttributes, getSpan, cancelSpan, cancelAllSpans, cancelSpansByPrefix};
export {startSpan, endSpan, endSpanWithAttributes, getSpan, cancelSpan, cancelAllSpans, cancelSpansByPrefix};
4 changes: 2 additions & 2 deletions src/pages/inbox/report/comment/TextCommentFragment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {containsOnlyCustomEmoji as containsOnlyCustomEmojiUtil, containsOnlyEmoj
import hydrateEmojiHtml from '@libs/hydrateEmojiHtml';
import Parser from '@libs/Parser';
import {getHtmlWithAttachmentID, getTextFromHtml} from '@libs/ReportActionsUtils';
import {tryEndSpan} from '@libs/telemetry/activeSpans';
import {endSpan} from '@libs/telemetry/activeSpans';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage';
Expand Down Expand Up @@ -67,7 +67,7 @@ function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsM
if (!reportActionID) {
return;
}
tryEndSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`);
endSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`);
}, [reportActionID]);

// If the only difference between fragment.text and fragment.html is <br /> tags and emoji tag
Expand Down
Loading