Conversation
PM-4168 fix open to work status
fix(PM-4085): hide biography section when description is absent
fix(PM-4133): Removed the availableForGigs for others
fix(PM-4202): Skills section completion logic update
fix(PM-4202): check for additional skills
| const createMmConfigClient = (dbUrl) => { | ||
| const Pool = getMmPoolConstructor() | ||
| const pool = new Pool({ | ||
| connectionString: dbUrl |
There was a problem hiding this comment.
[correctness]
Consider adding error handling for the connection pool creation to handle potential connection errors gracefully.
| throw new Error('MM config lookup requires at least one selected field') | ||
| } | ||
|
|
||
| const result = await pool.query( |
There was a problem hiding this comment.
[❗❗ security]
The use of template literals for SQL queries can lead to SQL injection vulnerabilities if not handled carefully. Ensure that all inputs are properly sanitized.
| WHERE "challengeId" = $1 | ||
| LIMIT 1 | ||
| `, | ||
| [String(challengeId)] |
There was a problem hiding this comment.
[❗❗ security]
Consider using parameterized queries to prevent SQL injection attacks. The current implementation directly interpolates values into the SQL string, which can be risky.
| resolvedTrackName: getUnifiedTrackName(row.trackName || row.trackId), | ||
| resolvedTypeName: getUnifiedTypeName(row.typeName || row.typeId) | ||
| })) | ||
| .filter(row => _.includes(['DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'COPILOT'], row.resolvedTrackName)) |
There was a problem hiding this comment.
[❗❗ correctness]
The filter condition for validRows in buildUnifiedStatsResponse now includes 'COPILOT'. However, the subsequent logic does not handle 'COPILOT' in the same way as other track names. Ensure that 'COPILOT' is handled appropriately in the response construction.
| resolvedTrackName: getUnifiedTrackName(row.trackName || row.trackId), | ||
| resolvedTypeName: getUnifiedTypeName(row.typeName || row.typeId) | ||
| })) | ||
| .filter(row => _.includes(['DEVELOP', 'DESIGN', 'DATA_SCIENCE'], row.resolvedTrackName)) |
There was a problem hiding this comment.
[correctness]
The filter condition for validRows in buildUnifiedStatsHistoryResponse excludes 'COPILOT', which is inconsistent with buildUnifiedStatsResponse. Verify if 'COPILOT' should be included or excluded in both functions for consistency.
| subTrackItem.rank = rank | ||
| } | ||
| subTrackItem.rank = rank | ||
| item.DEVELOP.subTracks.push(subTrackItem) |
There was a problem hiding this comment.
[💡 performance]
The assignment subTrackItem.rank = rank is unconditional, even if rank is an empty object. Consider omitting the rank property if it is empty to avoid unnecessary data in the response.
| newSchoolRank: h.newSchoolRank | ||
| }, _.isNil) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
[💡 maintainability]
The history array in buildUnifiedStatsHistoryResponse is constructed with _.omitBy, which may include empty objects if all properties are omitted. Consider filtering out empty objects to ensure cleaner data.
|
|
||
| // Biography Section | ||
| const biography = member.description || (basicInfo && basicInfo.shortBio) | ||
| const biography = member.description |
There was a problem hiding this comment.
[correctness]
The removal of the fallback (basicInfo && basicInfo.shortBio) for the biography variable might lead to missing biography information if member.description is not available. Consider retaining the fallback to ensure the biography section is populated when possible.
| const challenges = stat.challenges ?? 0 | ||
| const valueText = `${wins} ${wins === 1 ? 'win' : 'wins'}, ${submissions} ${submissions === 1 ? 'submission' : 'submissions'}, ${challenges} ${challenges === 1 ? 'challenge' : 'challenges'}` | ||
| const isCompetitiveProgramming = stat.trackName === 'Competitive Programming' | ||
| const rating = stat.rating == null ? 0 : stat.rating |
There was a problem hiding this comment.
[correctness]
The use of == null for checking stat.rating, stat.wins, stat.competitions, stat.submissions, and stat.challenges could lead to unintended behavior if these values are undefined. Consider using === null || stat.rating === undefined to explicitly check for both null and undefined.
| const config = require('config') | ||
| const { Pool } = require('pg') | ||
|
|
||
| const reviewDb = config.REVIEW_DB_URL |
There was a problem hiding this comment.
[❗❗ correctness]
The current implementation initializes the Pool only if REVIEW_DB_URL is defined. However, if REVIEW_DB_URL is misconfigured or incorrect, this will silently fail by setting reviewDb to null. Consider adding error handling to log or throw an error if REVIEW_DB_URL is not properly set, to avoid silent failures.
| LIMIT 1 | ||
| `) | ||
|
|
||
| const schemaName = result.rows[0] && result.rows[0].schemaName |
There was a problem hiding this comment.
[❗❗ correctness]
Accessing result.rows[0] without checking if result.rows is non-empty could lead to a runtime error if the query returns no rows. Consider checking result.rows.length before accessing the first element.
|
|
||
| const schemaName = result.rows[0] && result.rows[0].schemaName | ||
| if (!schemaName) { | ||
| throw new Error('REVIEW_DB_URL does not expose challengeResult. Verify REVIEW_DB_URL points to the review-api database and that review-api-v6 migrations have been deployed.') |
There was a problem hiding this comment.
[💡 maintainability]
The error message could be more informative by including the actual REVIEW_DB_URL value or additional context to aid debugging.
|
|
||
| function addLookupEntry (map, key, value) { | ||
| const normalizedKey = normalizeLookupKey(key) | ||
| if (!normalizedKey || value === null || value === undefined || map.has(normalizedKey)) { |
There was a problem hiding this comment.
[💡 maintainability]
The condition map.has(normalizedKey) prevents overwriting existing entries, which is good for preventing accidental overwrites. However, consider logging or handling cases where a duplicate key is detected to ensure that the system behaves as expected and to aid in debugging.
| try { | ||
| return await cachedLookupPromise | ||
| } catch (error) { | ||
| cachedLookupPromise = null |
There was a problem hiding this comment.
[maintainability]
Consider logging the error before rethrowing it. This can help with debugging by providing more context about what went wrong when the promise fails.
| * @param {Object} res the response | ||
| */ | ||
| async function refreshMemberStats (req, res) { | ||
| const result = await service.refreshMemberStats(req.authUser, req.params.handle, req.body) |
There was a problem hiding this comment.
[correctness]
Consider adding error handling for the service.refreshMemberStats call to ensure that any errors are properly caught and handled, preventing potential unhandled promise rejections.
| * @param {Object} res the response | ||
| */ | ||
| async function rerateMemberStats (req, res) { | ||
| const result = await service.rerateMemberStats(req.authUser, req.params.handle, req.body) |
There was a problem hiding this comment.
[correctness]
Consider adding error handling for the service.rerateMemberStats call to ensure that any errors are properly caught and handled, preventing potential unhandled promise rejections.
| return value | ||
| } | ||
|
|
||
| if (typeof global.BigInt !== 'function') { |
There was a problem hiding this comment.
[💡 maintainability]
Using global.BigInt directly may not be necessary. Consider using BigInt directly unless there's a specific reason to access it via global. This could improve readability and maintainability.
| async function rerateDevTrack (membersClient, challengeClient, reviewDbClient, userId, fromChallengeId) { | ||
| if (!reviewDbClient) { | ||
| throw new Error('REVIEW_DB_URL must be configured to rerate development stats') | ||
| } |
There was a problem hiding this comment.
[correctness]
The error message 'REVIEW_DB_URL must be configured to rerate development stats' suggests a missing configuration. Ensure that this configuration is validated earlier in the application lifecycle to prevent runtime errors.
|
|
||
| let startIndex = 0 | ||
| if (fromChallengeId) { | ||
| startIndex = targetHistory.findIndex((entry) => entry.challengeId === String(fromChallengeId)) |
There was a problem hiding this comment.
[correctness]
The logic assumes fromChallengeId is always valid if provided. Consider validating fromChallengeId before using it to find the start index to prevent potential errors.
| for (let index = startIndex; index < targetHistory.length; index += 1) { | ||
| const historyEntry = targetHistory[index] | ||
| const participantRows = (await fetchParticipantsForChallenge(reviewDbClient, historyEntry.challengeId)) | ||
| .filter((row) => parseBooleanLike(row.rated) !== false) |
There was a problem hiding this comment.
[correctness]
The filtering of participantRows using parseBooleanLike(row.rated) !== false could lead to unexpected results if row.rated is undefined. Consider explicitly checking for true to ensure clarity.
| ratingsUpdated += 1 | ||
| recomputedMaxRating = Math.max(recomputedMaxRating, updatedTarget.rating) | ||
|
|
||
| await membersClient.$transaction(async (tx) => { |
There was a problem hiding this comment.
[design]
The transaction logic is correct, but consider adding error handling within the transaction to manage potential failures gracefully.
| * memberStatsHistory, and memberMaxRating. | ||
| */ | ||
|
|
||
| 'use strict' |
There was a problem hiding this comment.
[💡 style]
The use of 'use strict' is not consistent with the rest of the codebase which uses double quotes for strings. Consider using double quotes for consistency.
| return value | ||
| } | ||
|
|
||
| if (typeof global.BigInt !== 'function') { |
There was a problem hiding this comment.
[💡 style]
Using global.BigInt is unnecessary here. You can directly use BigInt as it is a global object in Node.js environments.
| * @returns {Date|null} normalized Date or null when parsing fails | ||
| */ | ||
| function normalizeDate (value, fallbackValue) { | ||
| const date = value ? new Date(value) : new Date(fallbackValue) |
There was a problem hiding this comment.
[correctness]
The normalizeDate function creates a new Date object even if the value is falsy and fallbackValue is also falsy, which will result in Invalid Date. Consider checking if both value and fallbackValue are falsy before creating a Date object.
| } | ||
|
|
||
| const challengeTimestamp = challengeEventDate.getTime() | ||
| let seedIndex = -1 |
There was a problem hiding this comment.
[correctness]
The variable seedIndex is initialized to -1 and updated in the loop, but it is not clear if it should remain -1 if no valid index is found. Ensure that this behavior is intentional.
| } | ||
|
|
||
| if ( | ||
| scoringConfig && |
There was a problem hiding this comment.
[💡 maintainability]
The condition scoringConfig.relativeScoringEnabled === false is redundant since relativeScoringEnabled is already set to true by default if config is not found. Consider simplifying this condition.
| } | ||
|
|
||
| const scoreDate = normalizeDate(row.reviewedDate, row.createdAt) | ||
| const eventDate = normalizeDate(challenge.endDate, scoreDate) |
There was a problem hiding this comment.
[💡 performance]
The normalizeDate function is called twice with the same parameters for eventDate and endDate. Consider storing the result in a variable to avoid redundant function calls.
| throw new Error('MM_DB_URL must be configured to rerate marathon match stats') | ||
| } | ||
|
|
||
| const normalizedUserId = toBigIntUserId(userId) |
There was a problem hiding this comment.
[💡 performance]
The toBigIntUserId function is called on userId which might already be a BigInt. Consider checking the type before conversion to avoid unnecessary operations.
| tx, | ||
| normalizedUserId, | ||
| historyEntry.challengeId, | ||
| targetStateBeforeRun.numRatings > 0 ? targetStateBeforeRun.rating : null, |
There was a problem hiding this comment.
[💡 readability]
The ternary operation targetStateBeforeRun.numRatings > 0 ? targetStateBeforeRun.rating : null could be simplified by ensuring targetStateBeforeRun.rating is always a number, possibly by initializing it to 0 in createDefaultState.
| } | ||
|
|
||
| function getWeight (rating, timesPlayed) { | ||
| let weight = 1 / (1 - ((INITIAL_WEIGHT - FINAL_WEIGHT) / (timesPlayed + 1) + FINAL_WEIGHT)) - 1 |
There was a problem hiding this comment.
[❗❗ correctness]
The calculation of weight could potentially result in a division by zero if timesPlayed is a large negative number. Consider adding a check to ensure timesPlayed is non-negative before performing this calculation.
| return | ||
| } | ||
|
|
||
| const actualRankMap = buildActualRankMap(comparisonParticipants) |
There was a problem hiding this comment.
[performance]
The map(createPreparedParticipant) call on comparisonParticipants is unnecessary since comparisonParticipants is already a list of participants. This could lead to redundant processing and should be optimized.
| } | ||
|
|
||
| function getRatingColor (rating) { | ||
| const numericRating = Number(rating) |
There was a problem hiding this comment.
[correctness]
The conversion of rating to a number using Number(rating) could lead to unexpected results if rating is not a valid number. Consider validating rating before conversion to ensure it is a valid number.
| DELETE_USER_SCOPE | ||
| } = require('config') | ||
| const constants = require('../app-constants') | ||
| const { STATS_REFRESH, STATS_RERATE } = MEMBERS |
There was a problem hiding this comment.
[correctness]
The destructuring of STATS_REFRESH and STATS_RERATE from MEMBERS assumes these constants are always defined. Consider adding validation to ensure these constants exist to prevent potential runtime errors.
| controller: 'StatisticsController', | ||
| method: 'refreshMemberStats', | ||
| auth: 'jwt', | ||
| scopes: [STATS_REFRESH, MEMBERS.ALL], |
There was a problem hiding this comment.
[❗❗ security]
The scopes array includes STATS_REFRESH and MEMBERS.ALL. Ensure that STATS_REFRESH is correctly defined and intended for use here, as incorrect scope definitions can lead to unauthorized access.
| controller: 'StatisticsController', | ||
| method: 'rerateMemberStats', | ||
| auth: 'jwt', | ||
| scopes: [STATS_RERATE, MEMBERS.ALL], |
There was a problem hiding this comment.
[❗❗ security]
The scopes array includes STATS_RERATE and MEMBERS.ALL. Verify that STATS_RERATE is correctly defined and intended for use here to prevent potential security issues.
There was a problem hiding this comment.
Pull request overview
This PR advances the unified stats migration by adding admin/M2M write endpoints (refresh + rerate), introducing reusable dimension/rating helpers, and expanding migration verification and PDF reporting to incorporate unified competitive programming ratings.
Changes:
- Added stats dimension normalization/lookup helpers plus shared review DB relation resolver and client.
- Implemented Qubits-based rerating engines for Development (Challenge) and Data Science (Marathon Match), and exposed new
/members/:handle/stats/refresh+/members/:handle/stats/rerateroutes/controllers with new scopes. - Expanded migration verification tooling/docs (scripts + runbooks), updated PDF activity stats to treat
DATA_SCIENCEas “Competitive Programming”, and added extensive unit tests.
Reviewed changes
Copilot reviewed 28 out of 30 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/common/statsDimensionHelper.js |
Canonicalizes track/type labels and builds cached lookups between UUIDs, labels, abbreviations, and legacy ids. |
src/common/reviewDbHelper.js / src/common/reviewDb.js |
Adds cached resolution of challengeResult relation and shared review DB pool. |
src/ratings/qubitsAlgorithm.js |
Implements Qubits rating math and rating color bands for rerate engines. |
src/ratings/developRatingEngine.js / src/ratings/mmRatingEngine.js |
Adds rerate engines that replay challenge history and persist stats/history/max rating updates. |
src/routes.js / src/controllers/StatisticsController.js / config/default.js |
Exposes refresh/rerate endpoints with new M2M scopes and required env vars. |
src/scripts/verifyStatsMigration.js |
Enhances migration parity checks (ratings, history counts/order, rank completeness, mostRecent sanity). |
src/services/MemberService.js / src/common/profileTemplate.js |
Updates PDF stats display (Competitive Programming) and merges unified stats ratings into PDF output. |
docs/swagger.yaml |
Documents new endpoints and clarifies unified stats/history fallback behavior. |
test/unit/* |
Adds unit coverage for dimension helpers, refresh/rerate behaviors, rerate engines, and recalc script helpers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * @returns {boolean} true when the participant should be included in rerating | ||
| */ | ||
| function isParticipantEligibleForRating (row) { | ||
| const passedReview = parseBooleanLike(row && row.passedReview) |
There was a problem hiding this comment.
[correctness]
The use of parseBooleanLike here assumes that row.passedReview can be a string or boolean. Ensure that row.passedReview is consistently typed to avoid unexpected behavior.
| return true | ||
| } | ||
|
|
||
| const placement = Number(row && row.placement) |
There was a problem hiding this comment.
[💡 readability]
The conversion of row.placement to a number and subsequent check for integer status could be simplified by directly checking if row.placement is a positive integer. Consider using Number.isInteger(row.placement) && row.placement > 0 for clarity.
| * @returns {boolean} true when the participant should be included in rerating | ||
| */ | ||
| function isParticipantEligibleForRating (row) { | ||
| const finalScore = Number(row && row.finalScore) |
There was a problem hiding this comment.
[correctness]
The conversion of row.finalScore to a Number without checking if row is defined could lead to unexpected results if row is null or undefined. Consider adding a check to ensure row is defined before accessing row.finalScore.
| } | ||
|
|
||
| const placement = Number(row && row.placement) | ||
| return Number.isInteger(placement) && placement > 0 |
There was a problem hiding this comment.
[correctness]
The conversion of row.placement to a Number without checking if row is defined could lead to unexpected results if row is null or undefined. Consider adding a check to ensure row is defined before accessing row.placement.
| should.equal(statsRow.volatility, expectedTarget.volatility) | ||
| should.equal(historyRow.oldRating, null) | ||
| should.equal(historyRow.newRating, expectedTarget.rating) | ||
| }) |
There was a problem hiding this comment.
[correctness]
Consider adding a check to ensure statsRow and historyRow are not undefined before accessing their properties. This will prevent potential runtime errors if the expected rows are not found.
| should.equal(historyRow.newRating === historyRow.oldRating, false) | ||
| should.equal(historyRow.newRating < historyRow.oldRating, true) | ||
| should.equal(statsRow.rating, historyRow.newRating) | ||
| }) |
There was a problem hiding this comment.
[correctness]
Ensure that statsRow and historyRow are not undefined before accessing their properties. This will prevent potential runtime errors if the expected rows are not found.
| } | ||
|
|
||
| const numericValue = helper.bigIntToNumber(value) | ||
| return Number.isFinite(Number(numericValue)) ? Number(numericValue) : null |
There was a problem hiding this comment.
[💡 performance]
The conversion of numericValue to a Number is done twice. Consider storing the result of Number(numericValue) in a variable to avoid redundant conversion.
| return value.getTime() | ||
| } | ||
|
|
||
| const timestamp = new Date(value).getTime() |
There was a problem hiding this comment.
[💡 performance]
The conversion of value to a Date object is done twice. Consider storing the Date object in a variable to avoid redundant conversion.
| * @returns {Object|null} normalized max rating candidate for API responses | ||
| */ | ||
| function resolveCurrentMaxRating (maxRating, statsRows) { | ||
| if (!Array.isArray(statsRows) || statsRows.length === 0) { |
There was a problem hiding this comment.
[correctness]
The function resolveCurrentMaxRating assumes that statsRows is an array of objects with specific properties. If statsRows can be undefined or contain unexpected structures, consider adding validation or error handling to ensure robustness.
| member.maxRating = _.omit(member.maxRating, | ||
| ['id', 'userId', ...auditFields]) | ||
| } | ||
| const statsRows = _.isArray(member.memberStats) ? member.memberStats : undefined |
There was a problem hiding this comment.
[💡 style]
The variable statsRows is assigned undefined if member.memberStats is not an array. Consider using null instead of undefined for consistency with other parts of the code.
| } | ||
| if (member.maxRating) { | ||
| item.maxRating = _.pick(member.maxRating, ['rating', 'track', 'subTrack', 'ratingColor']) | ||
| const maxRating = buildCurrentMaxRatingResponse(member.maxRating, validRows) |
There was a problem hiding this comment.
[correctness]
The function buildCurrentMaxRatingResponse is called with validRows as the second argument. Ensure that validRows contains the necessary fields expected by resolveCurrentMaxRating to avoid potential runtime errors.
| } | ||
| if (_.includes(selectFields, 'maxRating')) { | ||
| prismaFilter.include.maxRating = true | ||
| prismaFilter.include.memberStats = { |
There was a problem hiding this comment.
[performance]
Including memberStats when maxRating is selected could potentially lead to performance issues if memberStats contains a large amount of data. Consider evaluating if this inclusion is necessary or if it can be optimized.
| const pageMembers = await prisma.member.findMany({ | ||
| where: { userId: { in: pageUserIds } }, | ||
| include: { maxRating: true, addresses: true } | ||
| include: { |
There was a problem hiding this comment.
[performance]
The addition of memberStats with a select clause in the include statement could potentially increase the complexity and size of the query result. Ensure that prismaHelper.currentMaxRatingStatsSelect is optimized to select only necessary fields to avoid performance degradation.
| maxRating: true, | ||
| addresses: true | ||
| addresses: true, | ||
| memberStats: { |
There was a problem hiding this comment.
[performance]
The inclusion of memberStats with a select clause in the include statement may lead to larger query results. Verify that prismaHelper.currentMaxRatingStatsSelect is selecting only essential fields to maintain query performance.
| ratingColor: true | ||
| } | ||
| }, | ||
| memberStats: { |
There was a problem hiding this comment.
[performance]
Adding memberStats with a select clause in the include statement can increase the data size returned by the query. Ensure that prismaHelper.currentMaxRatingStatsSelect is optimized to select only necessary fields to prevent performance issues.
| firstName: member.firstName || '', | ||
| lastName: member.lastName || '', | ||
| maxRating: member.maxRating ? _.pick(member.maxRating, ['rating', 'track', 'subTrack', 'ratingColor']) : null | ||
| maxRating: prismaHelper.buildCurrentMaxRatingResponse(member.maxRating, member.memberStats) |
There was a problem hiding this comment.
[❗❗ correctness]
The function prismaHelper.buildCurrentMaxRatingResponse now takes memberStats as an additional parameter. Ensure that this function handles cases where memberStats might be null or undefined to avoid potential runtime errors.
|
|
||
| prismaHelper.convertMember(member) | ||
|
|
||
| member.maxRating.should.deep.equal({ |
There was a problem hiding this comment.
[correctness]
The test assumes that convertMember will modify member.maxRating.ratingColor to '#9D9FA0', but this value is hardcoded in the test without explanation. Ensure that the logic in convertMember correctly derives this value, or clarify why this specific color is expected.
| ratingColor: '#9D9FA0' | ||
| }) | ||
| should.equal(member.userId, 100000218) | ||
| should.equal(member.createdAt, new Date('2026-03-26T00:00:00.000Z').getTime()) |
There was a problem hiding this comment.
[correctness]
The comparison of member.createdAt and member.updatedAt using getTime() could lead to false negatives if the convertMember function modifies these properties to a different Date object with the same timestamp. Consider comparing the timestamps directly or ensuring the test accounts for object identity if relevant.
| ) | ||
|
|
||
| should.equal(response.length, 1) | ||
| response[0].maxRating.should.deep.equal({ |
There was a problem hiding this comment.
[❗❗ correctness]
The test expects maxRating.ratingColor to be '#9D9FA0', but the mock data returns '#FCD617' for maxRating.ratingColor. Ensure the mock data aligns with the expected test outcome to avoid false positives.
| ] | ||
| } | ||
|
|
||
| throw new Error(`Unexpected query: ${query}`) |
There was a problem hiding this comment.
[💡 maintainability]
Throwing a generic Error with a message may not provide enough context for debugging. Consider using a custom error type or including more details in the error message to aid in troubleshooting.
| deleteMany: (args) => ({ action: 'deleteMany', args }), | ||
| upsert: (args) => ({ action: 'upsert', args }) | ||
| }, | ||
| $transaction: async (queries) => { |
There was a problem hiding this comment.
[❗❗ correctness]
Ensure that the $transaction method correctly handles rollback scenarios in case of an error during the transaction. This is crucial for maintaining data integrity.
| } | ||
| }) | ||
| transactionCalls[0][1].action.should.equal('upsert') | ||
| transactionCalls[0][1].args.where.userId.should.equal(global.BigInt(123)) |
There was a problem hiding this comment.
[maintainability]
The upsert operation uses a hardcoded ratingColor. Consider deriving this value dynamically based on the rating to ensure consistency and avoid potential mismatches.
| if (options.fetchStub) { | ||
| global.fetch = options.fetchStub | ||
| } else { | ||
| delete global.fetch |
There was a problem hiding this comment.
[maintainability]
Deleting global.fetch when options.fetchStub is not provided could lead to unexpected behavior if other parts of the application rely on fetch. Consider setting it to a no-op function instead.
| if (typeof originalFetch === 'undefined') { | ||
| delete global.fetch | ||
| } else { | ||
| global.fetch = originalFetch |
There was a problem hiding this comment.
[correctness]
Restoring global.fetch to originalFetch is correct, but ensure that originalFetch is always defined. If global.fetch was initially undefined, this could introduce unexpected behavior.
| return String(value) | ||
| .replace(/"/g, '"') | ||
| .replace(/'/g, '\'') | ||
| .replace(/&/g, '&') |
Check failure
Code scanning / CodeQL
Double escaping or unescaping High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 3 months ago
In general, to avoid double unescaping when decoding HTML entities, you must unescape the escape character (&) last. That way, substrings like &quot; remain as-is until after all other entity patterns (", ', etc.) have been processed and will not accidentally be turned into a fresh entity that is then decoded again.
The best minimal fix here is to change the order of .replace calls in decodeBasicHtmlEntities so that & is handled after all the other entities. Functionality remains the same for correctly encoded strings (they still end up decoded to the same characters), but inputs like &quot; will now decode to " instead of ", avoiding double-unescape. Concretely, inside src/services/StatisticsService.js, in the decodeBasicHtmlEntities function (lines 111–124), move .replace(/&/g, '&') to come after the .replace calls for <, >, , ", and '. No new methods or imports are needed.
| @@ -116,10 +116,10 @@ | ||
| return String(value) | ||
| .replace(/"/g, '"') | ||
| .replace(/'/g, '\'') | ||
| .replace(/&/g, '&') | ||
| .replace(/</g, '<') | ||
| .replace(/>/g, '>') | ||
| .replace(/ /g, ' ') | ||
| .replace(/&/g, '&') | ||
| .trim() | ||
| } | ||
|
|
No description provided.