Skip to content

fix(wasm): check if wasm enabled and no errors#1762

Merged
L2D2Grafana merged 6 commits intomainfrom
l2d2/wasm-fallback
Feb 26, 2026
Merged

fix(wasm): check if wasm enabled and no errors#1762
L2D2Grafana merged 6 commits intomainfrom
l2d2/wasm-fallback

Conversation

@L2D2Grafana
Copy link
Collaborator

@L2D2Grafana L2D2Grafana commented Feb 23, 2026

Follow up to #1757 & https://raintank-corp.slack.com/archives/C075BDBTX96/p1771844319780149

This is a more thoughtful implementation for a fallback if there is an error with the augurs library. isWasmInit checks if webassembly is supported and if there was an error initialization grafana/augurs. Updated augurs to version 0.10.2 which should include the fallback when instantiateStreaming fails.

Testing

  1. Don't change anything and setWasmInit is true
  2. Observe the sorting modes
Screenshot 2026-02-23 at 1 09 13 PM
  1. Throw and error or set setWasmInit(false);
  2. Observe only fallback sorting modes
Screenshot 2026-02-23 at 1 08 49 PM

@L2D2Grafana L2D2Grafana marked this pull request as ready for review February 23, 2026 22:02
@L2D2Grafana L2D2Grafana requested a review from a team as a code owner February 23, 2026 22:02
Copilot AI review requested due to automatic review settings February 23, 2026 22:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the WebAssembly initialization error handling by introducing explicit tracking of WASM initialization success. It replaces direct calls to wasmSupported() with a new isWasmInit() function that checks both WebAssembly support AND successful initialization of the augurs library. When WASM is unavailable or initialization fails, the UI automatically hides ML-based sorting options (changepoint and outliers) and falls back to standard deviation sorting.

Changes:

  • Added setWasmInit() and isWasmInit() functions to track WASM initialization state
  • Moved SortBy type definition from SortByScene.tsx to services/sorting.ts for centralized management
  • Updated sorting UI to dynamically filter out WASM-dependent options when WASM is unavailable
  • Added test coverage for WASM failure scenarios

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/services/sorting.ts Added WASM initialization tracking with setWasmInit(), isWasmInit(), and getDefaultSortBy() functions; exported SortBy type and constants
src/module.tsx Updated to call setWasmInit() with success/failure status after attempting to initialize WASM libraries
src/Components/ServiceScene/Breakdowns/SortByScene.tsx Updated constructor to fallback to stdDev when stored preference requires WASM but WASM is unavailable; dynamically filters WASM-dependent options from UI
src/Components/ServiceScene/Breakdowns/SortByScene.test.tsx Added test verifying WASM-dependent options are hidden when initialization fails
src/services/store.ts Updated import to use SortBy type from services/sorting
src/services/fields.ts Updated import to use SortBy type from services/sorting
src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx Updated to use getDefaultSortBy() and DEFAULT_SORT_DIRECTION constants
src/Components/ServiceScene/Breakdowns/LabelBreakdownScene.tsx Updated to use getDefaultSortBy() and DEFAULT_SORT_DIRECTION constants
src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx Updated to use getDefaultSortBy() and DEFAULT_SORT_DIRECTION constants
src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx Updated to use getDefaultSortBy() and DEFAULT_SORT_DIRECTION constants

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

} catch (e) {
console.warn('grafana-lokiexplore-app: WebAssembly init failed, ML sorting disabled.', e);
setWasmInit(false);
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

When WASM is not supported (wasmSupported() returns false), the wasmInitSucceeded flag is never set to false. This means that if WASM is not supported, isWasmInit() will continue to return false (the default value), which is correct, but it would be clearer and more explicit to call setWasmInit(false) in an else block after the wasmSupported() check. This would make the intent clearer and ensure the flag is explicitly set in all code paths.

Suggested change
}
}
} else {
setWasmInit(false);

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is false by default.

Copy link

@nicwestvold nicwestvold left a comment

Choose a reason for hiding this comment

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

@L2D2Grafana, here is some (LLM) feedback. It seemed reasonable.

PR Review: l2d2/wasm-fallback

Overall: The approach is sound — the shift from wasmSupported() (browser check only) to isWasmInit() (captures actual initialization success) is the right fix. The fallback to stdDev, UI filtering of ML
options, and constructor preference override are all well-handled. A few issues worth addressing before merge.


Bugs / Correctness

[Medium] memoize cache is blind to WASM state — stale result risk
src/services/sorting.ts:75-91

The cache key for sortSeries includes sortBy and direction but not wasmInitSucceeded. If sortSeries(series, 'changepoint', 'desc') were ever called before WASM initialized, the try-catch fallback on
line 49 silently mutates sortBy to ReducerID.stdDev and the stdDev-sorted result gets cached under a key containing 'changepoint'. A later call with the same args after WASM succeeds returns the stale
result.

In the current flow this can't happen (WASM is fully initialized before the App component loads), but the invariant is unprotected. Recommend appending _${isWasmInit()} to the cache key on line 90.


[Low] isWasmInit() in Component render is not reactive
src/Components/ServiceScene/Breakdowns/SortByScene.tsx:113

const wasmInit = isWasmInit();

This reads a plain module-level boolean  not Grafana Scenes state  so no state change will trigger a re-render. Safe today because WASM is initialized exactly once before any scene renders, but the ordering
invariant is implicit. A comment documenting it would prevent a future regression if retry/re-init logic is ever added.

---
Missing Test Coverage

[Medium] No test for stored-preference override in constructor
src/Components/ServiceScene/Breakdowns/SortByScene.test.tsx

The most important regression path of this PR  "user has 'changepoint' stored in localStorage, WASM fails on load, constructor should fall back to stdDev"  has no test. Suggested case:

test('Falls back to stdDev when stored preference is changepoint but WASM init failed', () => {
  setSortByPreference('fields', 'changepoint', 'desc');
  setWasmInit(false);
  scene = new SortByScene({ target: 'fields' });
  expect(scene.state.sortBy).toBe(ReducerID.stdDev);
});

[Low] New sorting.ts exports have no unit tests
src/services/sorting.test.ts

setWasmInit, isWasmInit, and getDefaultSortBy are untested. At minimum getDefaultSortBy should be tested for both branches.

---
Nits

Duplicate test names  SortByScene.test.tsx:46,56
Both tests are named 'Reports criteria changes'. Line 56 tests direction change and should be renamed to 'Reports direction changes'.

Hardcoded 'changepoint' in test assertion  SortByScene.test.tsx:63
expect(eventSpy).toHaveBeenCalledWith(new SortCriteriaChanged('fields', 'changepoint', 'asc'), true);
Should use the DEFAULT_SORT_BY constant to stay in sync if the value ever changes.

setWasmInit(false) in catch is redundant  module.tsx:34
The flag defaults to false and is only set to true on success, so this call is a no-op. Harmless, but a comment explaining "explicit reset in case of partial initialization" would clarify intent.

defaultOptions re-allocated every render  SortByScene.tsx:114-121
When WASM is not initialized, .map(...).filter(...) creates new arrays on every render. Since isWasmInit() is stable after init, a useMemo([wasmInit]) would avoid the repeated allocation.

console.warn vs. logger  module.tsx:33
Rest of the codebase uses the logger service for errors. console.warn may be intentional here given the module entry-point bundle size constraint, but worth a comment.

---
What's Well Done

- Default wasmInitSucceeded = false means the fallback is automatic if setWasmInit(true) is never reached (e.g., unsupported browser)  no extra guard needed.
- All 4 breakdown scenes (FieldValues, Fields, Label, LabelValues) consistently updated  no missed call sites.
- Moving SortBy type and constants to services/sorting.ts removes the circular dependency risk from importing them via SortByScene.
- E2E selector fixes (getByLabel, modal-scoped getByRole) are strictly better and avoid false matches.
- The constructor finalSortBy guard correctly handles the persisted-preference-but-WASM-failed scenario at the point of object creation.

---
Summary

| Severity |            File            |                            Issue                            |
| Medium   | sorting.ts:75-91           | memoize key missing WASM state                              |
| Medium   | SortByScene.test.tsx       | Missing test: stored preference override in constructor     |
| Low      | SortByScene.tsx:113        | isWasmInit() not reactive  document the ordering invariant |
| Low      | sorting.test.ts            | No unit tests for new exports                               |
| Nit      | SortByScene.test.tsx:46,56 | Duplicate test names                                        |
| Nit      | SortByScene.test.tsx:63    | Hardcoded 'changepoint' instead of DEFAULT_SORT_BY          |
| Nit      | module.tsx:34              | Redundant setWasmInit(false)                                |
| Nit      | SortByScene.tsx:114        | defaultOptions re-allocated each render                     |
| Nit      | module.tsx:33              | console.warn vs. logger inconsistency                       |

@L2D2Grafana
Copy link
Collaborator Author

@L2D2Grafana, here is some (LLM) feedback. It seemed reasonable.

Okay I updated per some of the comments, mostly the memoize one. It's not actually a problem since the app is only initialized once, but I suppose it helps with testing.

@nicwestvold
Copy link

@L2D2Grafana I'm ready to approve, but it looks like there is a test failure with 12.4.0. Here is what claude had to say:


CI Failure Analysis

Why only 12.4.0?

skipUnlessLatestGrafana skips tests on versions below GRAFANA_LATEST_SUPPORTED_VERSION = '12.3.1'. So 11.6, 12.0, and 12.1 skip the savedSearches tests entirely — they don't pass, they skip. The tests actually run on 12.3.4, 12.4.0, and dev only.

Root cause

The PR changed two assertions in savedSearches.spec.ts:

Before (both test assertions, lines ~104 and ~249):

page.getByRole('button', { name: /Edit filter with key service_name/ })
  .filter({ hasText: 'tempo-ingester' })

After:

page.getByLabel('Edit filter with key service_name')
// ...toContainText('tempo-ingester')

The failing assertion receives "service_name = tempo-distributor" instead of "tempo-ingester". The element is found — it's just the wrong service.

In Grafana 12.4.0 the ad-hoc filter chip structure appears to have changed: getByLabel('Edit filter with key service_name') matches the chip element, but the chip's text contains "tempo-distributor". This indicates the label format 'Edit filter with key service_name' may now match something other than the active filter chip — possibly an input, a label element, or a chip rendered differently in 12.4.0 that happens to show an earlier filter value.

The old selector was resilient to this because it combined role + regex name + filter({ hasText: 'tempo-ingester' }), meaning it would only match if the element also contained the expected service name. With the new approach, the first matching element is found regardless of its value, and then toContainText fails.

Fix

Revert the two filter chip assertions back to the original getByRole + filter style which is inherently self-asserting:

// tests/savedSearches.spec.ts ~line 104

await expect(
  page.getByRole('button', { name: /Edit filter with key service_name/ })
    .filter({ hasText: 'tempo-ingester' })
).toBeVisible();

The deleteButton scoping change (line ~155, scoped to modal) was a valid improvement and should be kept.

@L2D2Grafana
Copy link
Collaborator Author

@L2D2Grafana, here is some (LLM) feedback. It seemed reasonable.

PR Review: l2d2/wasm-fallback

Overall: The approach is sound — the shift from wasmSupported() (browser check only) to isWasmInit() (captures actual initialization success) is the right fix. The fallback to stdDev, UI filtering of ML options, and constructor preference override are all well-handled. A few issues worth addressing before merge.

Bugs / Correctness

[Medium] memoize cache is blind to WASM state — stale result risk src/services/sorting.ts:75-91

The cache key for sortSeries includes sortBy and direction but not wasmInitSucceeded. If sortSeries(series, 'changepoint', 'desc') were ever called before WASM initialized, the try-catch fallback on line 49 silently mutates sortBy to ReducerID.stdDev and the stdDev-sorted result gets cached under a key containing 'changepoint'. A later call with the same args after WASM succeeds returns the stale result.

In the current flow this can't happen (WASM is fully initialized before the App component loads), but the invariant is unprotected. Recommend appending _${isWasmInit()} to the cache key on line 90.

[Low] isWasmInit() in Component render is not reactive src/Components/ServiceScene/Breakdowns/SortByScene.tsx:113

const wasmInit = isWasmInit();

This reads a plain module-level boolean  not Grafana Scenes state  so no state change will trigger a re-render. Safe today because WASM is initialized exactly once before any scene renders, but the ordering
invariant is implicit. A comment documenting it would prevent a future regression if retry/re-init logic is ever added.

---
Missing Test Coverage

[Medium] No test for stored-preference override in constructor
src/Components/ServiceScene/Breakdowns/SortByScene.test.tsx

The most important regression path of this PR  "user has 'changepoint' stored in localStorage, WASM fails on load, constructor should fall back to stdDev"  has no test. Suggested case:

test('Falls back to stdDev when stored preference is changepoint but WASM init failed', () => {
  setSortByPreference('fields', 'changepoint', 'desc');
  setWasmInit(false);
  scene = new SortByScene({ target: 'fields' });
  expect(scene.state.sortBy).toBe(ReducerID.stdDev);
});

[Low] New sorting.ts exports have no unit tests
src/services/sorting.test.ts

setWasmInit, isWasmInit, and getDefaultSortBy are untested. At minimum getDefaultSortBy should be tested for both branches.

---
Nits

Duplicate test names  SortByScene.test.tsx:46,56
Both tests are named 'Reports criteria changes'. Line 56 tests direction change and should be renamed to 'Reports direction changes'.

Hardcoded 'changepoint' in test assertion  SortByScene.test.tsx:63
expect(eventSpy).toHaveBeenCalledWith(new SortCriteriaChanged('fields', 'changepoint', 'asc'), true);
Should use the DEFAULT_SORT_BY constant to stay in sync if the value ever changes.

setWasmInit(false) in catch is redundant  module.tsx:34
The flag defaults to false and is only set to true on success, so this call is a no-op. Harmless, but a comment explaining "explicit reset in case of partial initialization" would clarify intent.

defaultOptions re-allocated every render  SortByScene.tsx:114-121
When WASM is not initialized, .map(...).filter(...) creates new arrays on every render. Since isWasmInit() is stable after init, a useMemo([wasmInit]) would avoid the repeated allocation.

console.warn vs. logger  module.tsx:33
Rest of the codebase uses the logger service for errors. console.warn may be intentional here given the module entry-point bundle size constraint, but worth a comment.

---
What's Well Done

- Default wasmInitSucceeded = false means the fallback is automatic if setWasmInit(true) is never reached (e.g., unsupported browser)  no extra guard needed.
- All 4 breakdown scenes (FieldValues, Fields, Label, LabelValues) consistently updated  no missed call sites.
- Moving SortBy type and constants to services/sorting.ts removes the circular dependency risk from importing them via SortByScene.
- E2E selector fixes (getByLabel, modal-scoped getByRole) are strictly better and avoid false matches.
- The constructor finalSortBy guard correctly handles the persisted-preference-but-WASM-failed scenario at the point of object creation.

---
Summary

| Severity |            File            |                            Issue                            |
| Medium   | sorting.ts:75-91           | memoize key missing WASM state                              |
| Medium   | SortByScene.test.tsx       | Missing test: stored preference override in constructor     |
| Low      | SortByScene.tsx:113        | isWasmInit() not reactive  document the ordering invariant |
| Low      | sorting.test.ts            | No unit tests for new exports                               |
| Nit      | SortByScene.test.tsx:46,56 | Duplicate test names                                        |
| Nit      | SortByScene.test.tsx:63    | Hardcoded 'changepoint' instead of DEFAULT_SORT_BY          |
| Nit      | module.tsx:34              | Redundant setWasmInit(false)                                |
| Nit      | SortByScene.tsx:114        | defaultOptions re-allocated each render                     |
| Nit      | module.tsx:33              | console.warn vs. logger inconsistency                       |

@L2D2Grafana I'm ready to approve, but it looks like there is a test failure with 12.4.0. Here is what claude had to say:

CI Failure Analysis

Why only 12.4.0?

skipUnlessLatestGrafana skips tests on versions below GRAFANA_LATEST_SUPPORTED_VERSION = '12.3.1'. So 11.6, 12.0, and 12.1 skip the savedSearches tests entirely — they don't pass, they skip. The tests actually run on 12.3.4, 12.4.0, and dev only.

Root cause

The PR changed two assertions in savedSearches.spec.ts:

Before (both test assertions, lines ~104 and ~249):

page.getByRole('button', { name: /Edit filter with key service_name/ })
  .filter({ hasText: 'tempo-ingester' })

After:

page.getByLabel('Edit filter with key service_name')
// ...toContainText('tempo-ingester')

The failing assertion receives "service_name = tempo-distributor" instead of "tempo-ingester". The element is found — it's just the wrong service.

In Grafana 12.4.0 the ad-hoc filter chip structure appears to have changed: getByLabel('Edit filter with key service_name') matches the chip element, but the chip's text contains "tempo-distributor". This indicates the label format 'Edit filter with key service_name' may now match something other than the active filter chip — possibly an input, a label element, or a chip rendered differently in 12.4.0 that happens to show an earlier filter value.

The old selector was resilient to this because it combined role + regex name + filter({ hasText: 'tempo-ingester' }), meaning it would only match if the element also contained the expected service name. With the new approach, the first matching element is found regardless of its value, and then toContainText fails.

Fix

Revert the two filter chip assertions back to the original getByRole + filter style which is inherently self-asserting:

// tests/savedSearches.spec.ts ~line 104

await expect(
  page.getByRole('button', { name: /Edit filter with key service_name/ })
    .filter({ hasText: 'tempo-ingester' })
).toBeVisible();

The deleteButton scoping change (line ~155, scoped to modal) was a valid improvement and should be kept.

Thanks, yeah the savedSearches e2e have been flaky and the compatibility was also failing!

@matyax matyax added this to the 1.0.38 milestone Feb 26, 2026
Copy link
Collaborator

@matyax matyax left a comment

Choose a reason for hiding this comment

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

Looking good! I'd appreciate renaming the initialization function because it's not properly reflecting the underlying situation.

src/module.tsx Outdated
if (wasmSupported()) {
try {
await Promise.all([initChangepoint(), initOutlier()]);
setWasmInit(true);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is technically incorrect, because there's no WASM initialization. It's either supported or not, and what's initialized is Augurs.

Suggested change
setWasmInit(true);
setWasmSortInit(true);

Copy link
Collaborator

Choose a reason for hiding this comment

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

Even better, since today we're using WASM but tomorrow it might be something else, and we don't need to update these functions just because the internal implementation changed.

Suggested change
setWasmInit(true);
setSortingInit(true);

},
"dependencies": {
"@bsull/augurs": "^0.6.0",
"@bsull/augurs": "^0.10.2",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Awesome. No breaking changes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope!

Copy link
Collaborator

@matyax matyax left a comment

Choose a reason for hiding this comment

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

🚀

Comment on lines +19 to +23
export const setWasmSortInit = (succeeded: boolean) => {
wasmInitSucceeded = succeeded;
};

export const isWasmInit = () => wasmInitSucceeded;
Copy link
Collaborator

@matyax matyax Feb 26, 2026

Choose a reason for hiding this comment

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

Suggested change
export const setWasmSortInit = (succeeded: boolean) => {
wasmInitSucceeded = succeeded;
};
export const isWasmInit = () => wasmInitSucceeded;
export const setSortingInit = (succeeded: boolean) => {
wasmInitSucceeded = succeeded;
};
export const isSortingInit = () => wasmInitSucceeded;

Worst case:

Suggested change
export const setWasmSortInit = (succeeded: boolean) => {
wasmInitSucceeded = succeeded;
};
export const isWasmInit = () => wasmInitSucceeded;
export const setWasmSortingInit = (succeeded: boolean) => {
wasmInitSucceeded = succeeded;
};
export const isWasmSortingInit = () => wasmInitSucceeded;

Apologies, multitasking a lot.

@L2D2Grafana L2D2Grafana merged commit 8344148 into main Feb 26, 2026
30 checks passed
@L2D2Grafana L2D2Grafana deleted the l2d2/wasm-fallback branch February 26, 2026 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants