Skip to content

Commit 75d1e2b

Browse files
test: fixes corner cases for metrics collection data (#30197)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** Tightens the declarative MetaMetrics expectations static scan in `collect-qa-stats.mjs` so QA stats include every event naming pattern used under `tests/helpers/analytics/expectations/`. The `metametrics` slice of `qa-stats.json` is built by regex-parsing expectation modules. Several **valid** patterns produced **no** counted event names, so Grafana / coverage metrics understated E2E analytics coverage even when expectations existed. ## Cases that were previously missed | Pattern | Example / where it showed up | Prior failure | |--------|------------------------------|----------------------| | **`{ name: SOME_CONST }`** (no comma before `}`) | Card expectations: `events: [{ name: CARD_BUTTON_VIEWED }, …]` | Parser required `name: Ident,` only; `}` after the identifier did not match. | | **Only `eventNames: [ CONST ]`**, no `events[].name` | `predict-geo-restriction.analytics.ts` (`Geo Blocked Triggered`) | Inline `eventNames` arrays were never read; only `name:`, global `onboardingEvents`, and `const … = […];` with an “event-ish” variable name were. | | **`eventNames: [ A, B, … ]`** as the **sole** or **extra** inline list | Onramp (`RAMPS_*`), opt-out, bridge/swap/predict smoke files, etc. | Same as above — redundant for files that also had `name:`, but **`eventNames`-only** specs were fully dropped. | **Not a regression / intentional:** **`eventNames: [ ...someArray ]`** — spread segments are ignored here because the referenced `const` (e.g. `transactionEventNames`, `expectedEventNames`) is still ingested via the existing **`const *Names* / event-ish`** array path. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** N/A ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** N/A <!-- [screenshots/recordings] --> ### **After** N/A <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the regex-based static scan that feeds Grafana QA metrics; mistakes could under/over-count events or fail the stats job, impacting CI observability but not app runtime behavior. > > **Overview** > Tightens `.github/scripts/collect-qa-stats.mjs` MetaMetrics static scanning so `qa-stats.json` captures more valid event-name patterns from declarative `*.analytics.ts` expectations. > > The parser now (1) recognizes `name: SOME_CONST` even when it is the last field in an object, (2) extracts inline `eventNames: [...]` lists using a simple balanced-bracket slice, and (3) centralizes list-token resolution (string literals, `onboardingEvents.*`, string consts) while skipping spread entries like `...someArray`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 76bc803. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 6375f80 commit 75d1e2b

1 file changed

Lines changed: 68 additions & 18 deletions

File tree

.github/scripts/collect-qa-stats.mjs

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
*
2323
* MetaMetrics (top-level `metametrics` namespace): static scan of
2424
* `tests/helpers/analytics/expectations/*.ts` plus `LEGACY_INLINE_METAMETRICS_PATHS`
25-
* for specs not yet using declarative expectations.
25+
* for specs not yet using declarative expectations. Event names are picked from
26+
* `name:` fields, `eventNames: [...]`, `onboardingEvents.*`, and event-ish `const` arrays.
2627
*
2728
* Example output:
2829
* {
@@ -348,7 +349,63 @@ function parseConstStringLiterals(source) {
348349
}
349350

350351
/**
351-
* Event names from declarative `*.analytics.ts` modules (onboarding refs, `name:` entries, event arrays).
352+
* Content between "[" and matching "]" at the same nesting depth (naive bracket count).
353+
*
354+
* @param {string} source
355+
* @param {number} openBracketIdx index of '[' opening the array
356+
* @returns {string|null}
357+
*/
358+
function sliceBalancedSquareBracketInner(source, openBracketIdx) {
359+
if (source[openBracketIdx] !== '[') return null;
360+
let depth = 0;
361+
for (let i = openBracketIdx; i < source.length; i += 1) {
362+
const c = source[i];
363+
if (c === '[') depth += 1;
364+
else if (c === ']') {
365+
depth -= 1;
366+
if (depth === 0) return source.slice(openBracketIdx + 1, i);
367+
}
368+
}
369+
return null;
370+
}
371+
372+
/**
373+
* Segment from CSV inside `eventNames:` or event-ish `const` arrays. Spread/rest is skipped —
374+
* duplicated by a sibling `const *Names*` list when present (e.g. `...transactionEventNames`).
375+
*
376+
* @param {string} token
377+
* @param {Record<string, string>} onboardingMap
378+
* @param {Record<string, string>} strConsts
379+
* @returns {string|null}
380+
*/
381+
function resolveDeclarativeExpectationListToken(token, onboardingMap, strConsts) {
382+
const t = token.replace(/^\s+|\s+$/g, '');
383+
if (!t) return null;
384+
const lit = t.match(/^['"]([^'"]+)['"]$/);
385+
if (lit) return lit[1];
386+
const onb = t.match(/^onboardingEvents\.(\w+)$/);
387+
if (onb && onboardingMap[onb[1]]) return onboardingMap[onb[1]];
388+
if (/^\.\.\.\s*\w+$/.test(t)) return null;
389+
if (strConsts[t]) return strConsts[t];
390+
return null;
391+
}
392+
393+
/**
394+
* @param {string} inner
395+
* @param {Record<string, string>} onboardingMap
396+
* @param {Record<string, string>} strConsts
397+
* @param {Set<string>} out
398+
*/
399+
function collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out) {
400+
for (const part of inner.split(',')) {
401+
const v = resolveDeclarativeExpectationListToken(part, onboardingMap, strConsts);
402+
if (v) out.add(v);
403+
}
404+
}
405+
406+
/**
407+
* Event names from declarative `*.analytics.ts`: `eventNames:` arrays, onboarding refs,
408+
* `name:` entries, string/const lookups, and event-ish `const [...]` declarations.
352409
*
353410
* @param {string} source
354411
* @param {Record<string, string>} onboardingMap
@@ -368,11 +425,18 @@ function collectFromDeclarativeExpectationsSource(source, onboardingMap, out) {
368425
const v = onboardingMap[m[1]];
369426
if (v) out.add(v);
370427
}
371-
for (const m of source.matchAll(/\bname:\s*(\w+)\s*,/g)) {
428+
// Allow `name: IDENT,` (more properties follow) or `name: IDENT }` (single-field expectation object).
429+
for (const m of source.matchAll(/\bname:\s*(\w+)\s*[},]/g)) {
372430
const v = strConsts[m[1]];
373431
if (v) out.add(v);
374432
}
375433

434+
for (const em of source.matchAll(/\beventNames:\s*\[/g)) {
435+
const openIdx = em.index + em[0].length - 1;
436+
const inner = sliceBalancedSquareBracketInner(source, openIdx);
437+
if (inner) collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out);
438+
}
439+
376440
const reArrays = /\bconst\s+(\w+)\s*=\s*\[([\s\S]*?)\];/g;
377441
let am;
378442
while ((am = reArrays.exec(source)) !== null) {
@@ -382,21 +446,7 @@ function collectFromDeclarativeExpectationsSource(source, onboardingMap, out) {
382446
/(?:event|Event|Expected|expectation|analytics|Names)/.test(varName) ||
383447
/\bonboardingEvents\b|\bexpectedEvents\b/.test(inner);
384448
if (!looksLikeEventList) continue;
385-
for (const part of inner.split(',')) {
386-
const t = part.replace(/^\s+|\s+$/g, '');
387-
if (!t) continue;
388-
const lit = t.match(/^['"]([^'"]+)['"]$/);
389-
if (lit) {
390-
out.add(lit[1]);
391-
continue;
392-
}
393-
const onb = t.match(/^onboardingEvents\.(\w+)$/);
394-
if (onb && onboardingMap[onb[1]]) {
395-
out.add(onboardingMap[onb[1]]);
396-
continue;
397-
}
398-
if (strConsts[t]) out.add(strConsts[t]);
399-
}
449+
collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out);
400450
}
401451
}
402452

0 commit comments

Comments
 (0)