You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-11-14] [$175] Insights - Uploaded avatars are missing from PNG exports #106119
Open Insights on staging with a dashboard showing members with uploaded avatars.
Export the dashboard as a PNG.
Open the downloaded image and check the browser console.
Expected Result
The PNG includes uploaded avatars and charts matching the dashboard, without avatar CSP violations.
Actual Result
Uploaded avatars are missing from the PNG, while default avatars render. The staging investigation identified a CSP error when fetching uploaded avatars.
Fix
The exporter fetches avatar images to embed them in the PNG. Allow https://d1wpcgnaa73g0y.cloudfront.net in the App Worker's connect-src directive, scoped to this distribution without broadening script permissions.
Insights opened on dev NewDot web, but the required dashboard with uploaded member avatars was unavailable. No staging/production verification was performed; no data or source was modified.
Step
Status
1. Open Insights with uploaded member avatarsInsights displayed its empty state. The required expense/member fixture was unavailable.
❌
2. Export dashboard as PNGBlocked by the missing dashboard fixture; no export attempted.
❌
3. Inspect PNG for avatars and chartsNot verified: no PNG exported; downloaded-image inspection was also unavailable through the tester.
❌
4. Check console for avatar CSP violationsNot verified: browser console access was unavailable through the tester.
❌
Proposal
What is the root cause of that problem?
Confidence: medium — code supports the reported staging diagnosis; end-to-end confirmation remains outstanding.
The dashboard can display uploaded avatars, but PNG export separately fetches their bytes for embedding. The App Worker's connect-src omits the reported CloudFront distribution, even though img-src permits displaying it. Failed image fetches can become transparent placeholders, explaining a downloaded PNG with missing uploaded avatars while locally rendered default avatars remain visible.
The inspected policy permits image display but lacks the avatar origin in connection permissions: csp.ts:70–135. Default/catalog avatars resolve locally: UserAvatarUtils.ts:402–422.
CORS or rendering problems could coexist, but do not explain the reported CSP refusal. Allowing the connection does not itself prove CDN responses permit cross-origin reads. Backend logs would not establish this browser-side failure.
What changes do you think we should make in order to solve the problem?
Use the existing open Allow avatar fetches for Insights PNG exports PR rather than creating a duplicate. It adds only https://d1wpcgnaa73g0y.cloudfront.net to connect-src, with staging/production policy regression tests; it does not broaden script permissions.
Fix scope and acceptance checks
Proposed permission: csp.ts:94–95. Proposed header tests: csp.test.ts:7–41. These were inspected, not executed in this analysis.
Deploy the App Worker, reload the document, then verify a populated dashboard's exported PNG includes uploaded avatars and charts without avatar CSP violations. Repeat on staging and production after their respective deployments; production impact is currently unconfirmed.
If fetching still fails after CSP permits it, inspect CDN CORS headers and redirects.
Correct the PR's offline expectation: an uncached avatar-fetch failure alone can produce a transparent placeholder rather than the generic error dialog.
The issue's separately linked PR-preview policy change was not independently inspected.
What alternative solutions did you explore? (Optional)
Changing img-src, broadening CloudFront permissions, or allowing scripts would not address the narrowly identified connection restriction safely. No exporter rewrite is indicated by the evidence.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/106119#issuecomment-6662620193) to create a draft PR. An existing fix PR is already open; prefer reviewing it rather than requesting a duplicate. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal. · view recording
changed the title [-]Insights - Uploaded avatars are missing from PNG exports[/-][+][$175] Insights - Uploaded avatars are missing from PNG exports[/+]on Nov 5, 2026
Please re-state the problem that we are trying to solve in this issue
When an Insights dashboard containing members with uploaded avatars is exported as a PNG, the uploaded avatars are missing from the downloaded image (default/letter avatars render fine). The staging investigation flagged a CSP error: the PNG exporter fetches the remote avatar bytes to embed them, but the avatar CDN origin is not permitted by the App Worker's connect-src.
What is the root cause of that problem?
Two layers, both in src/pages/Insights/exportInsightsDashboard/index.ts:
Layer 1 — the exporter only pre-rasterizes <canvas> charts, never avatar <img>s (lines 30–38)
Before calling domToCanvas, the export pre-converts chart canvases to inline data URLs so the clone is self-contained:
// src/pages/Insights/exportInsightsDashboard/index.ts:30-38constchartImages=elements.map((element)=>Array.from(element.querySelectorAll('canvas, img'),(canvas)=>{if(!(canvasinstanceofHTMLCanvasElement)){// <-- only canvases handledreturnundefined;// <-- avatars (HTMLImageElement) fall through}const{width, height}=canvas.getBoundingClientRect();return{src: canvas.toDataURL('image/png'), width, height};}),);
The querySelectorAll('canvas, img')does collect avatar <img> elements (the comment at line 50 even says "Count avatars too so chart indices stay aligned"), but the guard on line 32 maps every non-<canvas> node to undefined. So in onCloneNode (lines 50–67) the loop skips them (if (!chartImage) continue;, line 53) and the cloned avatar <img> keeps its original remote src.
Layer 2 — domToCanvas (modern-screenshot) then tries to fetch that remote src and is blocked by connect-src
Because the avatar <img> in the clone still points at its remote URL (e.g. https://d1wpcgnaa73g0y.cloudfront.net/…), the modern-screenshot rasterizer has to fetch() the bytes to embed them (embed-image-element.ts in the library fetches remote image srcs and substitutes a transparent placeholder on failure). That fetch() is governed by connect-src, notimg-src. The App Worker CSP (web/cloudflare-worker/src/csp.ts:70–135) lists img-src: ['*', …] (so the avatar displays on screen) but connect-src does not include https://d1wpcgnaa73g0y.cloudfront.net — so the embed-time fetch fails, the avatar becomes a transparent placeholder, and the exported PNG is missing it.
This is why uploaded avatars (remote CloudFront URL → AvatarImage renders <Image source={{uri: …}}> → web <img> with remote src, per src/components/Avatar/primitives/AvatarImage.tsx:38 and src/components/Avatar/UserAvatar.tsx:77) fail, while default avatars render: defaults are returned as local SVG components by getAvatar (src/libs/UserAvatarUtils.ts:412–423) and inline in the DOM, needing no fetch. The Insights dashboard uses exactly this <UserAvatar source={item.avatar} /> in src/pages/Insights/charts/InsightsDataTable.tsx:90–93.
The existing fix PR #106018 addresses only Layer 2 (adding the CloudFront origin to connect-src). That unblocks the fetch for this one CDN, but leaves the exporter dependent on (a) the third-party library's fetch path and (b) every future avatar/CDN origin also being added to CSP. The robust fix addresses Layer 1: pre-rasterize the avatar <img> to a data URL the same way canvases already are, so no runtime fetch — and therefore no CSP dependency — is needed at all.
What changes do you think we should make in order to solve the problem?
Primary (Layer 1): pre-embed avatar <img> elements as data URLs before domToCanvas. In exportInsightsDashboard/index.ts, extend the chartImages capture so HTMLImageElement nodes that are remote (and already loaded) are rasterized to a data URL via a canvas, exactly parallel to the canvas path:
// Replace the chartImages builder (lines 30-38) with one that also handles <img>:constchartImages=elements.map((element)=>Array.from(element.querySelectorAll('canvas, img'),(node)=>{if(nodeinstanceofHTMLCanvasElement){const{width, height}=node.getBoundingClientRect();return{src: node.toDataURL('image/png'), width, height};}if(nodeinstanceofHTMLImageElement){const{width, height}=node.getBoundingClientRect();// Cross-origin <img> can't be drawn straight to a canvas without CORS taint;// re-fetch the bytes (same-origin-ish via the CDN) and convert to a data URL.// If anything fails, leave undefined and let the CSP/fetch path handle it.try{// Prefer the already-cached bitmap if the image loaded cleanly and same-origin permits.constc=document.createElement('canvas');c.width=node.naturalWidth||width||1;c.height=node.naturalHeight||height||1;constctx=c.getContext('2d');if(!ctx)returnundefined;ctx.drawImage(node,0,0,c.width,c.height);return{src: c.toDataURL('image/png'), width, height};}catch{// Canvas tainted (cross-origin without CORS) — fall back to fetch+data URL below, or undefined.returnundefined;}}returnundefined;}),);
Then in onCloneNode, the existing replacement loop (lines 52–67) already swaps each captured node by index — so once the undefined for avatars is replaced with a real data URL, the cloned <img> gets src = "data:image/png;base64,…", which domToCanvas embeds with no fetch and no CSP dependency. (For the cross-origin/tainted case, fall back to a fetch(src).then(r => r.blob()) → FileReader → data URL; this still needs connect-src, so the CSP addition from #106018 is kept as the fallback, not the primary path.)
Secondary (Layer 2, keep PR #106018): add https://d1wpcgnaa73g0y.cloudfront.net to connect-src in web/cloudflare-worker/src/csp.ts and extend web/cloudflare-worker/tests/csp.test.ts to assert it for staging/production. This covers the fetch-fallback for tainted canvases and any other in-DOM remote images, and matches the issue's stated fix.
Net effect: avatars embed even if the CDN URL changes or a new avatar origin is used, and the CSP change closes the remaining cross-origin fetch gap.
What specific scenarios should be covered by automated tests?
tests/unit/exportInsightsDashboard*:
Given a dashboard DOM with one <canvas> chart + one <img> avatar (remote src), after capture the avatar's cloned node has a data:image/pngsrc (no network fetch in the embed path).
An avatar <img> whose draw-to-canvas throws (tainted) falls back to the fetch→data-URL path and, with CSP permitting, still embeds; without CSP it degrades to the placeholder (assert the fallback is reachable, not silent).
Default (SVG) avatars still embed unchanged.
web/cloudflare-worker/tests/csp.test.ts: staging + production connect-src contains https://d1wpcgnaa73g0y.cloudfront.net with no wildcard/script broadening.
Manual/staging: export a populated dashboard with uploaded avatars; assert the PNG contains them and the console shows no CSP violations.
What alternative solutions did you explore? (Optional)
CSP-only fix (PR [No QA] Allow avatar fetches for Insights PNG exports on dev #106018 as the whole answer). Necessary but insufficient: it fixes today's CDN but couples every avatar-host change to a CSP deploy, and still routes every export through a runtime cross-origin fetch that can race/taint. Pre-embedding removes the fetch entirely.
Configure the avatar CDN to send Access-Control-Allow-Origin and set crossOrigin="anonymous" on <img>. Would avoid canvas taint, but requires backend/CDN config changes and touches the avatar render path outside the export scope. Good long-term companion to the primary fix.
Replace modern-screenshot's embed with a custom rasterizer. Over-engineered; pre-embedding the <img> nodes reuses the library for everything else.
changed the title [-][$175] Insights - Uploaded avatars are missing from PNG exports[/-][+][Due for payment 2026-11-14] [$175] Insights - Uploaded avatars are missing from PNG exports[/+]on Nov 7, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.33-1 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-11-14. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
Activity
MelvinBot commentedon Nov 5, 2026
🤖 Web test steps verification results:⚠️ partial
Insights opened on dev NewDot web, but the required dashboard with uploaded member avatars was unavailable. No staging/production verification was performed; no data or source was modified.
1. Open Insights with uploaded member avatars
Insights displayed its empty state. The required expense/member fixture was unavailable.2. Export dashboard as PNG
Blocked by the missing dashboard fixture; no export attempted.3. Inspect PNG for avatars and charts
Not verified: no PNG exported; downloaded-image inspection was also unavailable through the tester.4. Check console for avatar CSP violations
Not verified: browser console access was unavailable through the tester.Proposal
What is the root cause of that problem?
Confidence: medium — code supports the reported staging diagnosis; end-to-end confirmation remains outstanding.
The dashboard can display uploaded avatars, but PNG export separately fetches their bytes for embedding. The App Worker's
connect-srcomits the reported CloudFront distribution, even thoughimg-srcpermits displaying it. Failed image fetches can become transparent placeholders, explaining a downloaded PNG with missing uploaded avatars while locally rendered default avatars remain visible.Code evidence and alternatives checked
modern-screenshot: exportInsightsDashboard/index.ts:19–46.What changes do you think we should make in order to solve the problem?
Use the existing open Allow avatar fetches for Insights PNG exports PR rather than creating a duplicate. It adds only
https://d1wpcgnaa73g0y.cloudfront.nettoconnect-src, with staging/production policy regression tests; it does not broaden script permissions.Fix scope and acceptance checks
What alternative solutions did you explore? (Optional)
Changing
img-src, broadening CloudFront permissions, or allowing scripts would not address the narrowly identified connection restriction safely. No exporter rewrite is indicated by the evidence.Possibly related: Insight-new console errors appear after restore the encoder and export again concerns the same export flow, but its reported console errors are not established as the same cause or a duplicate.
Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/106119#issuecomment-6662620193)to create a draft PR. An existing fix PR is already open; prefer reviewing it rather than requesting a duplicate.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal. · view recording
melvin-bot commentedon Nov 5, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @truph01 (
External)[-]Insights - Uploaded avatars are missing from PNG exports[/-][+][$175] Insights - Uploaded avatars are missing from PNG exports[/+]melvin-bot commentedon Nov 5, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022118372058403676674
camesenin commentedon Nov 5, 2026
melvin-bot commentedon Nov 5, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
vanshshah11 commentedon Nov 7, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue
When an Insights dashboard containing members with uploaded avatars is exported as a PNG, the uploaded avatars are missing from the downloaded image (default/letter avatars render fine). The staging investigation flagged a CSP error: the PNG exporter fetches the remote avatar bytes to embed them, but the avatar CDN origin is not permitted by the App Worker's
connect-src.What is the root cause of that problem?
Two layers, both in
src/pages/Insights/exportInsightsDashboard/index.ts:Layer 1 — the exporter only pre-rasterizes
<canvas>charts, never avatar<img>s (lines 30–38)Before calling
domToCanvas, the export pre-converts chart canvases to inline data URLs so the clone is self-contained:The
querySelectorAll('canvas, img')does collect avatar<img>elements (the comment at line 50 even says "Count avatars too so chart indices stay aligned"), but the guard on line 32 maps every non-<canvas>node toundefined. So inonCloneNode(lines 50–67) the loop skips them (if (!chartImage) continue;, line 53) and the cloned avatar<img>keeps its original remotesrc.Layer 2 —
domToCanvas(modern-screenshot) then tries to fetch that remotesrcand is blocked byconnect-srcBecause the avatar
<img>in the clone still points at its remote URL (e.g.https://d1wpcgnaa73g0y.cloudfront.net/…), themodern-screenshotrasterizer has tofetch()the bytes to embed them (embed-image-element.tsin the library fetches remote imagesrcs and substitutes a transparent placeholder on failure). Thatfetch()is governed byconnect-src, notimg-src. The App Worker CSP (web/cloudflare-worker/src/csp.ts:70–135) listsimg-src: ['*', …](so the avatar displays on screen) butconnect-srcdoes not includehttps://d1wpcgnaa73g0y.cloudfront.net— so the embed-time fetch fails, the avatar becomes a transparent placeholder, and the exported PNG is missing it.This is why uploaded avatars (remote CloudFront URL →
AvatarImagerenders<Image source={{uri: …}}>→ web<img>with remotesrc, persrc/components/Avatar/primitives/AvatarImage.tsx:38andsrc/components/Avatar/UserAvatar.tsx:77) fail, while default avatars render: defaults are returned as local SVG components bygetAvatar(src/libs/UserAvatarUtils.ts:412–423) and inline in the DOM, needing no fetch. The Insights dashboard uses exactly this<UserAvatar source={item.avatar} />insrc/pages/Insights/charts/InsightsDataTable.tsx:90–93.The existing fix PR #106018 addresses only Layer 2 (adding the CloudFront origin to
connect-src). That unblocks thefetchfor this one CDN, but leaves the exporter dependent on (a) the third-party library's fetch path and (b) every future avatar/CDN origin also being added to CSP. The robust fix addresses Layer 1: pre-rasterize the avatar<img>to a data URL the same way canvases already are, so no runtime fetch — and therefore no CSP dependency — is needed at all.What changes do you think we should make in order to solve the problem?
Primary (Layer 1): pre-embed avatar
<img>elements as data URLs beforedomToCanvas. InexportInsightsDashboard/index.ts, extend thechartImagescapture soHTMLImageElementnodes that are remote (and already loaded) are rasterized to a data URL via a canvas, exactly parallel to the canvas path:Then in
onCloneNode, the existing replacement loop (lines 52–67) already swaps each captured node by index — so once theundefinedfor avatars is replaced with a real data URL, the cloned<img>getssrc = "data:image/png;base64,…", whichdomToCanvasembeds with no fetch and no CSP dependency. (For the cross-origin/tainted case, fall back to afetch(src).then(r => r.blob())→FileReader→ data URL; this still needsconnect-src, so the CSP addition from #106018 is kept as the fallback, not the primary path.)Secondary (Layer 2, keep PR #106018): add
https://d1wpcgnaa73g0y.cloudfront.nettoconnect-srcinweb/cloudflare-worker/src/csp.tsand extendweb/cloudflare-worker/tests/csp.test.tsto assert it for staging/production. This covers the fetch-fallback for tainted canvases and any other in-DOM remote images, and matches the issue's stated fix.Net effect: avatars embed even if the CDN URL changes or a new avatar origin is used, and the CSP change closes the remaining cross-origin fetch gap.
What specific scenarios should be covered by automated tests?
tests/unit/exportInsightsDashboard*:<canvas>chart + one<img>avatar (remotesrc), after capture the avatar's cloned node has adata:image/pngsrc(no network fetch in the embed path).<img>whose draw-to-canvas throws (tainted) falls back to the fetch→data-URL path and, with CSP permitting, still embeds; without CSP it degrades to the placeholder (assert the fallback is reachable, not silent).web/cloudflare-worker/tests/csp.test.ts: staging + productionconnect-srccontainshttps://d1wpcgnaa73g0y.cloudfront.netwith no wildcard/script broadening.What alternative solutions did you explore? (Optional)
Access-Control-Allow-Originand setcrossOrigin="anonymous"on<img>. Would avoid canvas taint, but requires backend/CDN config changes and touches the avatar render path outside the export scope. Good long-term companion to the primary fix.modern-screenshot's embed with a custom rasterizer. Over-engineered; pre-embedding the<img>nodes reuses the library for everything else.PR
PR will be linked here once created.
Contributor details
Your Expensify account email: svansh1111@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~01b6a593fd295a20bf
melvin-bot commentedon Nov 7, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
jeffrey-graham commentedon Nov 7, 2026
melvin-bot commentedon Nov 7, 2026
[-][$175] Insights - Uploaded avatars are missing from PNG exports[/-][+][Due for payment 2026-11-14] [$175] Insights - Uploaded avatars are missing from PNG exports[/+]melvin-bot commentedon Nov 7, 2026
@truph01
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.33-1 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-11-14. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
melvin-bot commentedon Nov 10, 2026
@truph01 Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!
melvin-bot commentedon Nov 12, 2026
@truph01 Eep! 4 days overdue now. Issues have feelings too...