Skip to content

[Due for payment 2026-07-21] [$250] [Sentry: APP-7MV] Web WebGL context creation failure on /home #93834

Description

@mountiny

Sentry

https://expensify.sentry.io/issues/APP-7MV

Impact (snapshot at filing)

  • Users (total since first seen): 527
  • Events: 6,337
  • Users (last 14d, 9.4.x releases): 96
  • First seen: 2026-03-15
  • Last seen: ongoing
  • Platform: web
  • App version(s): 9.4.5–9.4.9
  • Affected route(s): /home
  • Mechanism: auto.browser.browserapierrors.setTimeout

Stack trace (top frames, first-party only)

Error: failed to create webgl context: err 0
(captured via Sentry withScope on /home)

Suspected cause

WebGL context creation fails on /home, likely due to GPU/browser restrictions or memory pressure on the client.

Reproduction

Unknown — see Sentry events linked from the Sentry issue.

Related

  • Prior GH issues: none
Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. self-assigned this
    on Jun 17, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    Help WantedApply this label when an issue is open to proposals by contributors
    BugSomething is broken. Auto assigns a BugZero manager.
    on Jun 17, 2026
  3. melvin-bot commented on Jun 17, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @truph01 (External)

  4. melvin-bot commented on Jun 17, 2026

    @melvin-bot
  5. godzillaxbox19982014-lgtm commented on Jun 17, 2026

    @godzillaxbox19982014-lgtm

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    [$250] [Sentry: APP-7MV] Web WebGL context creation failure on /home

    What is the root cause of that problem?

    I traced this to the path that handles APP, 7MV, WebGL:

    • src/libs/telemetry/trackExpenseCreationError.ts:84 (warn): Log.warn('[trackExpenseCreationError] Failed to track expense creation error', {sentryError, originalContext: context});
    • src/libs/telemetry/trackExpenseCreationError.ts:136 (warn): Log.warn('[trackExpenseApiError] Failed to track expense API error', {sentryError, originalContext: context});
    • src/libs/telemetry/trackExpenseCreationError.ts:43 (trackExpenseCreationError): function trackExpenseCreationError(error: Error | null, context: ExpenseCreationErrorContext): void {
    • The local evidence matches this recurring failure shape: native callback assumes current policy/report still exists. Here, src/libs/telemetry/trackExpenseCreationError.ts:84 and src/libs/telemetry/trackExpenseCreationError.ts:136 do not agree on the same APP, 7MV, WebGL value, so the user-visible path can show a missing or stale state even though nearby data already has the needed context.

    What changes do you think we should make in order to solve the problem?

    • Update src/libs/telemetry/trackExpenseCreationError.ts:84 to derive APP, 7MV, WebGL from the matched app state path that owns this flow before the value is consumed.
    • Carry the same normalized value through src/libs/telemetry/trackExpenseCreationError.ts:136; that follows the safer family move to add the guard at the ownership boundary, not only at render, so the write/optimistic/read paths agree instead of only changing the visible component.
    • Keep the change inside the Sentry Crash, Null/Undefined, Native Hybrid surface and avoid changing do not hide crashes with blanket catch, native platform differences.

    What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?

    1. Add or update a regression in tests/unit/**/*.ts for the exact issue flow: [$250] [Sentry: APP-7MV] Web WebGL context creation failure on /home.
    2. Cover the counter-case near src/libs/telemetry/trackExpenseCreationError.ts:84 so valid existing APP, 7MV, WebGL behavior still works.
    3. Manually repeat the issue steps after the patch and confirm the visible state/action is no longer stale.

    What alternative solutions did you explore? (Optional)

    • I would not add a broad new condition outside these referenced paths; the safer fix is to keep APP, 7MV, WebGL consistent at the first read/write boundary shown above.
  6. MelvinBot commented on Jun 17, 2026

    @MelvinBot
    Contributor

    Proposal

    🤖 Automated analysis — confidence: High for the root cause, Medium for the exact fix (the failure is environmental, so the goal is graceful degradation, not "making WebGL work").

    What is the root cause of that problem?

    /home renders SpendOverTimeSection, which mounts a Skia‑backed bar chart. On web, every @shopify/react-native-skia <Canvas> (via victory-native's CartesianChart) creates a GPU surface with CanvasKit's MakeWebGLCanvasSurface. When the browser can't hand out a WebGL context — GPU blocklisted, hardware acceleration off, too many live contexts, or memory pressure — CanvasKit throws failed to create webgl context: err 0 and does not fall back to software rendering. The throw is unhandled, so it lands in Sentry (captured from a setTimeout/rAF tick, hence auto.browser.browserapierrors.setTimeout).

    Evidence chain
    1. The exact string lives only in canvaskit-wasm — node_modules/canvaskit-wasm/bin/full/canvaskit.js. MakeWebGLCanvasSurface does n=this.GetWebGLContext(E,v); if(!n||0>n) throw "failed to create webgl context: err "+n;. The software fallback (MakeSWCanvasSurface) only runs if MakeOnScreenGLSurface returns null — not when GetWebGLContext returns 0, so a missing context throws instead of degrading.
    2. react-native-skia's web view calls it without catching: @shopify/react-native-skia/src/views/SkiaPictureView.web.tsx:62 (WebGLRenderer.onResize), then re-throws at line 68.
    3. /home mounts the chart: src/pages/home/HomePage.tsx:76 → SpendOverTimeSectionContent.tsx → BarChart/index.tsx → BarChartContent.tsx (CartesianChart from victory-native).
    4. There is no ErrorBoundary and no WebGL‑availability check around the chart, so the library exception propagates uncaught.

    This is purely a web‑GPU/environment limitation; the chart is the first WebGL surface on /home, which is why this route is over‑represented. The existing godzillaxbox19982014-lgtm proposal pointing at trackExpenseCreationError.ts is unrelated — that file has nothing to do with WebGL or charts.

    What changes do you think we should make in order to solve the problem?

    We can't make WebGL work on a machine that refuses it, but we can stop the uncaught error and degrade gracefully. Recommended (in order of preference):

    1. Guard with a WebGL pre‑check before mounting the chart. In BarChart/index.tsx (web), probe document.createElement('canvas').getContext('webgl2') once; if unavailable, render the existing fallback/empty state instead of the Skia chart. Cheap and avoids ever calling into CanvasKit.
    2. Wrap the chart in an ErrorBoundary (src/components/ErrorBoundary) with a graceful fallback, so any surface‑creation throw shows a non‑broken UI rather than bubbling to Sentry. This also covers context‑loss after mount (MakeWebGLCanvasSurface on resize at SkiaPictureView.web.tsx:62).

    Do both 1 and 2 for full coverage (pre‑mount + post‑mount). A patch-package patch making the library fall back to MakeSWCanvasSurface is also viable but is harder to maintain across upgrades.

    What alternative solutions did you explore? (Optional)

    • Patching canvaskit-wasm/react-native-skia to fall back to software rendering — works but adds a maintenance burden and software rendering may be slow; an error boundary is simpler and library‑version‑agnostic.
    • Suppressing the Sentry event only — hides the symptom but leaves users with a silently missing chart; not recommended.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/93834) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.

  7. x-dev90 commented on Jun 17, 2026

    @x-dev90
    Contributor

    Proposal

    What is the root cause of that problem?

    The /home route mounts SpendOverTimeSection as part of the Home page layout ([HomePage.tsx](https://github.com/Expensify/App/blob/18d0e4e0ba5928991588296714fd9f00a8425d07/src/pages/home/[HomePage.tsx](https://github.com/Expensify/App/blob/18d0e4e0ba5928991588296714fd9f00a8425d07/src/pages/home/HomePage.tsx#L85)#L75), HomePage.tsx). When that section is loading or ready, it renders SearchChartView ([SpendOverTimeSectionContent.tsx](

    {(state === SPEND_OVER_TIME_STATE.LOADING || state === SPEND_OVER_TIME_STATE.READY) && (
    )), which dispatches to one of the shared Skia-backed chart components ([SearchChartView.tsx](https://github.com/Expensify/App/blob/18d0e4e0ba5928991588296714fd9f00a8425d07/src/components/Search/[SearchChartView.tsx](https://github.com/Expensify/App/blob/18d0e4e0ba5928991588296714fd9f00a8425d07/src/components/Search/SearchChartView.tsx#L83)#L37), SearchChartView.tsx).

    On web, the chart entry points load Skia/CanvasKit through WithSkiaWeb, but they only provide a Suspense loading fallback and do not provide a local error boundary ([BarChart/index.tsx](

    ), [LineChart/index.tsx]( ), [PieChart/index.tsx]( )). If CanvasKit cannot create a WebGL context, the lazy load/render error bubbles to the app-level error boundary, which captures the crash for /home.

    What changes do you think we should make in order to solve the problem?

    Add a shared SkiaWebLoader wrapper for web Skia chart entry points. It should preserve the existing loading fallback, but wrap WithSkiaWeb in a local ErrorBoundary that handles Skia/CanvasKit/WebGL initialization failures with an inline chart fallback instead of allowing the optional chart widget to crash the entire page.

    Required changes:

    • Add src/components/Charts/SkiaWebLoader.tsx.
    • Move the repeated WithSkiaWeb loading UI into that shared loader.
    • Add a Skia-specific fallback that catches WebGL/CanvasKit/Skia errors and renders an inline chart error state.
    • Re-throw non-Skia errors so unrelated chart bugs still reach the normal app error boundary.
    • Replace direct WithSkiaWeb usage in BarChart, LineChart, PieChart, and VictoryChartRenderer with SkiaWebLoader.

    Tests that should be added:

    • Unit test SkiaWebLoader with a mocked WithSkiaWeb throwing failed to create webgl context: err 0, and verify the chart error fallback renders.
    • Unit test that a non-Skia error is re-thrown instead of being swallowed.
    • Existing Home page rendering coverage can stay unchanged unless a Home integration test is added for SpendOverTimeSection with a mocked chart failure.

    The existing points at src/libs/telemetry/trackExpenseCreationError.ts, but that file is unrelated to /home chart rendering and does not participate in the Skia/CanvasKit WebGL initialization path. Changing expense telemetry would not stop the WebGL context error from bubbling out of the Home chart tree.

    I also considered adding a guard only inside SpendOverTimeSection, but that would leave the same WebGL crash possible in Search chart views and HTML-rendered Victory charts. The shared loader is the minimal root-cause fix because all current web Skia chart entry points use the same dependency boundary.

    What alternative solutions did you explore? (Optional)

  8. yusufdeveloper2903 commented on Jun 17, 2026

    @yusufdeveloper2903
    Contributor

    Proposal

    What is the root cause of that problem?

    The exact string failed to create webgl context: err 0 is not thrown by mapbox-gl. It exists only in canvaskit-wasm (the Skia WebAssembly backend), inside MakeWebGLCanvasSurface:

    // node_modules/canvaskit-wasm/bin/full/canvaskit.js
    n = this.GetWebGLContext(E, v);
    if (!n || 0 > n) throw "failed to create webgl context: err " + n;

    mapbox-gl@2.15.0 does something different on context failure — it fires new ErrorEvent(new Error('Failed to initialize WebGL')) (a different message, and an event, not a throw). So the error is a Skia/CanvasKit failure, not a map failure.

    CanvasKit is mounted on web by @shopify/react-native-skia. On /home, the chart in SpendOverTimeSection is rendered directly:

    <SpendOverTimeSection />

    SpendOverTimeSection → BarChart, which loads CanvasKit via WithSkiaWeb and then mounts a victory-native chart backed by a Skia <Canvas>:

    return (
    <WithSkiaWeb
    opts={{locateFile: (file: string) => `/${file}`}}
    getComponent={getBarChartContent}
    componentProps={props}
    fallback={
    <View style={styles.chartWebFallback}>
    <ActivityIndicator
    size="large"
    reasonAttributes={reasonAttributes}
    />
    </View>
    }
    />
    );

    On web, Skia's SkiaPictureView.web creates the GPU surface in WebGLRenderer.onResize by calling CanvasKit.MakeWebGLCanvasSurface(canvas). When the browser refuses a WebGL context — GPU blocklisted, hardware acceleration off, the per‑page live‑context cap reached, or memory pressure — GetWebGLContext returns 0 and CanvasKit throws the raw string above. Crucially, onResize is invoked from the component's onLayout handler and the requestAnimationFrame redraw tick, not from React's render phase. That is why it is reported with mechanism auto.browser.browserapierrors.setTimeout (Sentry wraps timers/rAF) and why it is uncaught.

    So: /home mounts a Skia chart → Skia calls MakeWebGLCanvasSurface inside an async onLayout/rAF tick → browser cannot allocate a WebGL context → CanvasKit throws failed to create webgl context: err 0 from an async callback → no React boundary can catch it → it lands in Sentry.

    Two consequences worth noting:

    1. An ErrorBoundary does not fix this. React error boundaries only catch errors during render/lifecycle, not errors thrown from a setTimeout/requestAnimationFrame/onLayout callback. The MapView already wraps its renderer in an ErrorBoundary and the same class of async GL error would still escape it:

      return (
      <ErrorBoundary
      resetKeys={[errorResetKey]}
      fallback={
      <PendingMapView
      title={isOffline ? translate('distance.mapPending.title') : translate('distance.mapPending.errorTitle')}
      subtitle={isOffline ? translate('distance.mapPending.subtitle') : translate('distance.mapPending.errorSubtitle')}
      style={styles.mapEditView}
      />
      }
      >

    2. BarChart is not the only entry point. Every Skia‑on‑web surface goes through WithSkiaWeb, and there are four identical mount points — fixing only BarChart leaves the other three throwing the same way:

      • BarChart —
        return (
        <WithSkiaWeb
        opts={{locateFile: (file: string) => `/${file}`}}
        getComponent={getBarChartContent}
        componentProps={props}
        fallback={
        <View style={styles.chartWebFallback}>
        <ActivityIndicator
        size="large"
        reasonAttributes={reasonAttributes}
        />
        </View>
        }
        />
        );
      • LineChart —
        <WithSkiaWeb
        opts={{locateFile: (file: string) => `/${file}`}}
        getComponent={getLineChartContent}
        componentProps={props}
        fallback={
        <View style={styles.chartWebFallback}>
        <ActivityIndicator
        size="large"
        reasonAttributes={reasonAttributes}
        />
        </View>
        }
        />
        );
      • PieChart —
        <WithSkiaWeb
        opts={{locateFile: (file: string) => `/${file}`}}
        getComponent={getPieChartContent}
        componentProps={props}
        fallback={
        <View style={styles.chartWebFallback}>
        <ActivityIndicator
        size="large"
        reasonAttributes={reasonAttributes}
        />
        </View>
        }
        />
        );
      • chat VictoryChartRenderer (charts inside report HTML) —
        return (
        <WithSkiaWeb
        opts={{locateFile: (file: string) => `/${file}`}}
        getComponent={getBaseVictoryChartRenderer}
        componentProps={props}
        fallback={
        <View style={styles.chartWebFallback}>
        <ActivityIndicator
        size="large"
        reasonAttributes={reasonAttributes}
        />
        </View>
        }
        />

      This is web‑only by construction: the native variant renders BaseVictoryChartRenderer directly with no WithSkiaWeb/WebGL, matching Sentry's Platform: web.

    What changes do you think we should make in order to solve the problem?

    Fail closed at the shared WithSkiaWeb convergence point: probe WebGL support before mounting Skia, and when it is unavailable render the fallback that each of these call sites already passes, instead of letting CanvasKit throw asynchronously.

    Add a tiny web‑only helper, e.g. isWebGLAvailable(), that memoizes a single probe:

    // returns false on GPU blocklist / hardware accel off / context-cap reached
    const canvas = document.createElement('canvas');
    return !!(canvas.getContext('webgl2') ?? canvas.getContext('webgl'));

    This mirrors the context types CanvasKit's GetWebGLContext requests (webgl2, then webgl), so a true result means MakeWebGLCanvasSurface will succeed. Then in each of the four web mount points, when !isWebGLAvailable() render the existing fallback View instead of WithSkiaWeb. The fallback is already in scope in every one of these files — the same styles.chartWebFallback block they already pass to WithSkiaWeb:

    fallback={
    <View style={styles.chartWebFallback}>
    <ActivityIndicator
    size="large"
    reasonAttributes={reasonAttributes}
    />
    </View>
    }

    This reuses the project's established "fail closed, render a fallback instead of crashing the report" pattern already present for malformed chart HTML:

    try {
    processedResult = processVictoryChartTree(tnode, fonts.typefaces.EXP_NEUE, null);
    } catch (error) {
    // Malformed chart HTML can make a parser throw. Fail closed (render nothing) instead of crashing the whole report.
    Log.warn('[VictoryChartRenderer] Failed to process chart tree from malformed HTML', {error});
    return null;
    }

    Because the guard sits at WithSkiaWeb (the single web convergence for Skia), it covers BarChart, LineChart, PieChart, and the chat VictoryChartRenderer at once, with no new copy and no behavior change on machines that do support WebGL. To avoid repeating the guard in four files, it can be factored into a thin shared web wrapper around WithSkiaWeb that the charts call instead; minor structuring can be discussed in the PR phase.

    What alternative solutions did you explore? (Optional)

    Wrapping each chart in an ErrorBoundary (as one might infer from the existing MapView.web boundary). This does not work here: the throw originates from Skia's requestAnimationFrame/onLayout redraw, not from render, so React never sees it — the error would still reach Sentry. A patch-package patch making Skia fall back to MakeSWCanvasSurface (software rendering) is also possible, but it adds upgrade‑maintenance cost and silently degrades to slow CPU rendering; the pre‑mount WebGL probe is cheaper, library‑version‑agnostic, and keeps the already‑designed fallback UI.

  9. github-actions commented on Jun 17, 2026

    @github-actions
    Contributor

    ⚠️ @yusufdeveloper2903 Thanks for your proposal. Please update it to follow the proposal template, as proposals are only reviewed if they follow that format (note the mandatory sections).

  10. 47 remaining items

  11. melvin-bot commented on Jul 15, 2026

    @melvin-bot

    Current assignee @mallenexpensify is eligible for the Awaiting Payment assigner, not assigning anyone new.

  12. mountiny commented on Jul 15, 2026

    @mountiny
    ContributorAuthor

    Sentry impact update (2026-07-15, 14d window)

    • Sentry: APP-7MV
    • Users (14d / total): 166 / 1113
    • Status: Still active on production builds 9.4.32–9.4.34
  13. mallenexpensify commented on Jul 22, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor: @wildan-m paid $250 via Upwork
    Contributor+: @truph01 due $250 via NewDot

    ^ Test case created. Thx

  14. mountiny commented on Jul 27, 2026

    @mountiny
    ContributorAuthor

    APP-7MV resurfaced on 9.4.43+ (/search + /home). New tracker filed: #97104 (prior #93834 paid/closed).

  15. garrettmknight commented on Sep 3, 2026

    @garrettmknight
    Contributor

    $250 approved for @truph01

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions