<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lê Huy Giang</title>
    <description>The latest articles on DEV Community by Lê Huy Giang (@lehuygiang28).</description>
    <link>https://dev.to/lehuygiang28</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1529231%2F37acb01d-54cf-4f96-ac27-81bd82ff62ea.jpeg</url>
      <title>DEV Community: Lê Huy Giang</title>
      <link>https://dev.to/lehuygiang28</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lehuygiang28"/>
    <language>en</language>
    <item>
      <title>I Already Had Sentry. Then an iPhone from 2018 White-Screened My Karaoke App.</title>
      <dc:creator>Lê Huy Giang</dc:creator>
      <pubDate>Sat, 22 Aug 2026 06:48:56 +0000</pubDate>
      <link>https://dev.to/lehuygiang28/i-already-had-sentry-then-an-iphone-from-2018-white-screened-my-karaoke-app-gdf</link>
      <guid>https://dev.to/lehuygiang28/i-already-had-sentry-then-an-iphone-from-2018-white-screened-my-karaoke-app-gdf</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;VKara&lt;/strong&gt; is a karaoke room in the browser.&lt;/p&gt;

&lt;p&gt;YouTube already plays the videos. VKara's job is the room: one TV as the playback screen, everyone's phone as a remote.&lt;/p&gt;

&lt;p&gt;I rebuilt that for the &lt;a href="https://dev.to/lehuygiang28/i-rebuilt-my-karaoke-app-so-everyones-phone-could-be-a-remote-4k8b"&gt;GitHub Finish-Up-A-Thon&lt;/a&gt;. The idea is simple enough to explain in one sentence. The users are not.&lt;/p&gt;

&lt;p&gt;A karaoke night at home usually involves parents, uncles, and aunts. People who are not going to update their phones. People whose "second phone" is still Safari 12. The TV in the living room is often a Tizen or Android TV from years ago. I do not own every one of those devices. I cannot sit in every living room and tap through first paint.&lt;/p&gt;

&lt;p&gt;That is why I added Sentry about a month ago — as &lt;a href="https://github.com/lehuygiang28/vkara/pull/4" rel="noopener noreferrer"&gt;foundation&lt;/a&gt;, not as a challenge badge. &lt;code&gt;vkara-web&lt;/code&gt; and &lt;code&gt;vkara-api&lt;/code&gt;. Real production. The point was to see what old phones and old TVs do when I am not in the room.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://vkara.vercel.app/en" rel="noopener noreferrer"&gt;https://vkara.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/lehuygiang28/vkara" rel="noopener noreferrer"&gt;https://github.com/lehuygiang28/vkara&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;Sentry's issue list is noisy. That is normal.&lt;/p&gt;

&lt;p&gt;Some events are not VKara bugs. YouTube's iframe hitting a Tizen same-origin wall. Zalo — a chat app a lot of people in Vietnam use — injecting &lt;code&gt;zaloJSV2&lt;/code&gt; into the page. I left those alone.&lt;/p&gt;

&lt;p&gt;Two first-party failures were taking the room down. (Sentry issue links need org login. The screenshots below are the public proof.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;White screen on iOS 12&lt;/strong&gt; — WEB-B, WEB-C.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwirnwmu92twlyuap9gp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwirnwmu92twlyuap9gp.gif" alt="Session Replay: blank vkara.vercel.app, 19s timeline with error markers. Activity: Intl.PluralRules (WEB-B) then addEventListener is not a function (WEB-C)" width="720" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;19 seconds. White screen. WEB-B and WEB-C in one Replay.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the device I would never have tested on purpose. Mobile Safari 12.1.2. iOS 12.5.8. Referrer: Yahoo Vietnam.&lt;/p&gt;

&lt;p&gt;Replay showed the whole night dying in under 20 seconds. &lt;code&gt;next-intl&lt;/code&gt; constructs &lt;code&gt;Intl.PluralRules&lt;/code&gt;. Then &lt;code&gt;matchMedia().addEventListener&lt;/code&gt;. Then the error boundary called &lt;code&gt;useI18n()&lt;/code&gt; to explain the crash — same Intl path. The safety net crashed too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frxu592en45rf9r39hm8u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frxu592en45rf9r39hm8u.png" alt="Sentry VKARA-WEB-B: TypeError new Intl.PluralRules. Mobile Safari 12.1.2 / iOS 12.5.8. Seer: PluralRules not supported on Safari 12" width="800" height="478"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Seer named the missing API. The error-boundary re-throw I found in &lt;code&gt;error.tsx&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related shelf 502&lt;/strong&gt; — API-A, API-C.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foq1daiyiyr0gqfn3cl5q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foq1daiyiyr0gqfn3cl5q.png" alt="Sentry VKARA-API-A: attributedDescription.content undefined. 189 events, POST /related, bun 1.3.13 production" width="800" height="478"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;code&gt;POST /related&lt;/code&gt;. InnerTube HTTP succeeded. youtubei threw on schema drift. Users=1 is a proxy IP. &lt;strong&gt;189 events&lt;/strong&gt; at screenshot time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The phone remote asks for related songs. The parser hit a missing YouTube field. The route turned that into &lt;code&gt;502 youtube_upstream_failed&lt;/code&gt;. The player could still be up. The remote felt broken.&lt;/p&gt;

&lt;p&gt;No new features. I wanted those two to stop killing a karaoke night.&lt;/p&gt;

&lt;p&gt;I am not going to quote "zero events since deploy."&lt;/p&gt;

&lt;p&gt;Safari 12 is a long tail. Two days of silence and two months of silence can mean the same thing: that phone did not open the room again. Supporting old devices needs users I do not have on a contest deadline. A quiet dashboard is not a result.&lt;/p&gt;

&lt;p&gt;The number that matters is what one event does to the room.&lt;/p&gt;

&lt;p&gt;WEB-B and WEB-C: &lt;strong&gt;9 events, 2 users&lt;/strong&gt;, last seen 6 Aug. Looks small. The Replay is 19 seconds of white screen. One phone opening the room is enough to take the night with it. Last-seen-in-August is how rare the device is, not how small the bug is.&lt;/p&gt;

&lt;p&gt;API-A: &lt;strong&gt;189 events&lt;/strong&gt; at triage. Users=1 is a proxy. That count is how often &lt;code&gt;/related&lt;/code&gt; turned a parser throw into &lt;code&gt;502&lt;/code&gt; while a song was already playing. I did not try to make the Sentry issue disappear. youtubei will keep drifting. After the fix, the same throw is &lt;code&gt;kind=parse&lt;/code&gt;, handled, warning. The remote gets &lt;code&gt;200&lt;/code&gt; and an empty shelf. Local check: real &lt;code&gt;videoId&lt;/code&gt; → 20 items; bad continuation → empty list, not 502.&lt;/p&gt;

&lt;p&gt;That is the impact I can defend before the next iPhone from 2018 shows up.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;PR: &lt;a href="https://github.com/lehuygiang28/vkara/pull/8" rel="noopener noreferrer"&gt;https://github.com/lehuygiang28/vkara/pull/8&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Fixes VKARA-WEB-B&lt;/code&gt; · &lt;code&gt;WEB-C&lt;/code&gt; · &lt;code&gt;API-A&lt;/code&gt; · &lt;code&gt;API-C&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I already had &lt;code&gt;tv-polyfills.js&lt;/code&gt; for Tizen / Chrome 76. It loads &lt;code&gt;beforeInteractive&lt;/code&gt; on every locale. Production showed Safari 12 needed two more shims. Native APIs stay untouched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PluralRules&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PluralRules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PluralRules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PluralRules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;other&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did not ship full CLDR plurals. Vietnamese does not need English-style plurals. A white screen is worse than &lt;code&gt;select()&lt;/code&gt; returning &lt;code&gt;'other'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The error boundary no longer imports &lt;code&gt;next-intl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LocaleError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useErrorBoundaryRecovery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RecoveryShell&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;documentRecoveryPhaseLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Related parse failures become an empty list. Sentry still gets &lt;code&gt;kind=parse&lt;/code&gt;. Real YouTube HTTP failures still 502:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeParseRelated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;BaseVideoParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseRelated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;asYoutubeRawData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;captureRelatedParseFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;The useful decision was what &lt;strong&gt;not&lt;/strong&gt; to fix.&lt;/p&gt;

&lt;p&gt;I did not chase Zalo inject or Tizen iframe SOP. Those show up in Sentry. They are not my product.&lt;/p&gt;

&lt;p&gt;I did not wrap every &lt;code&gt;matchMedia&lt;/code&gt; call with &lt;code&gt;addListener&lt;/code&gt;. There are two call sites today. There will be a third. One prototype shim in the file that already runs before app JS is enough.&lt;/p&gt;

&lt;p&gt;I did not keep &lt;code&gt;useI18n()&lt;/code&gt; behind try/catch. Hooks cannot do that. The recovery label is a small &lt;code&gt;en&lt;/code&gt; / &lt;code&gt;vi&lt;/code&gt; map from &lt;code&gt;document.documentElement.lang&lt;/code&gt;. Ugly. Correct.&lt;/p&gt;

&lt;p&gt;I did not turn every &lt;code&gt;/related&lt;/code&gt; error into &lt;code&gt;200&lt;/code&gt;. An empty shelf looks like "no recommendations." A 502 looks like the app is down. During a song, those are not the same thing.&lt;/p&gt;

&lt;p&gt;What I learned: nobody at a karaoke night cares that &lt;code&gt;Intl.PluralRules&lt;/code&gt; shipped in Safari 13.&lt;/p&gt;

&lt;p&gt;They care that the TV shows a video, and related still returns a list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;A month ago I wired Sentry as &lt;a href="https://github.com/lehuygiang28/vkara/pull/4" rel="noopener noreferrer"&gt;production foundation&lt;/a&gt; because VKara runs on devices I do not have on my desk: old iPhones, old Samsung TVs, Android TV boxes that never get a browser update. If something white-screens in a living room, I only find out if the client tells me.&lt;/p&gt;

&lt;p&gt;What surprised me was the community support. Node is obvious. Bun, I expected they would get to. Seeing a first-party &lt;a href="https://docs.sentry.io/platforms/javascript/guides/elysia/" rel="noopener noreferrer"&gt;Sentry guide for Elysia&lt;/a&gt; still caught me. That is the kind of coverage that makes foundation work boring in a good way: &lt;code&gt;Sentry.init()&lt;/code&gt;, wrap the app, ship. I was not writing a custom adapter for a karaoke API.&lt;/p&gt;

&lt;p&gt;This challenge is the first time that bet paid off in a way I can point at.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What I actually used it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error monitoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WEB-B: Safari 12.1.2 / iOS 12.5.8 — the phone I do not own. API-A: 189 events at triage, kept as handled parse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session Replay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WEB-B and WEB-C are one white screen, not two stories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Seer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Named &lt;code&gt;Intl.PluralRules&lt;/code&gt;. Missed that &lt;code&gt;error.tsx&lt;/code&gt; would re-throw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;From the editor: pull API-A, keep WEB-A / Zalo off the PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seer first returned HTTP 402. No budget on the plan I had. I used &lt;code&gt;bugsmash26&lt;/code&gt;, then WEB-B RCA ran on the issue page.&lt;/p&gt;

&lt;p&gt;Same reason there's no before/after chart above — a quiet week doesn't prove a long-tail device is fixed. What Sentry proved instead: the 19 seconds, and that API-A now reports as &lt;code&gt;kind=parse&lt;/code&gt; instead of 502.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://antigravity.google" rel="noopener noreferrer"&gt;Antigravity&lt;/a&gt; with &lt;strong&gt;Gemini&lt;/strong&gt; and the &lt;strong&gt;Sentry MCP&lt;/strong&gt;. Not to generate the PR. To sit next to the issue list and ask what was actually mine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrcx1i9qbr2b3l6yo832.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrcx1i9qbr2b3l6yo832.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Connect to org &lt;code&gt;vkara&lt;/code&gt;, find the issue, explain cause.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://mcp.sentry.dev/?ide=gemini" rel="noopener noreferrer"&gt;Sentry MCP setup&lt;/a&gt; page currently shows Gemini CLI. Antigravity wants &lt;code&gt;serverUrl&lt;/code&gt; instead of &lt;code&gt;url&lt;/code&gt;. One field. Then it connected. If Sentry adds an Antigravity snippet later, that extra minute goes away. Not a real problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sentry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"serverUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.sentry.dev/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemini + MCP pulled Seer on &lt;strong&gt;API-A&lt;/strong&gt;: InnerTube schema drift, &lt;code&gt;videoInfo.attributedDescription&lt;/code&gt; undefined inside youtubei &lt;code&gt;Video.load&lt;/code&gt;. That is the &lt;code&gt;/related&lt;/code&gt; 502.&lt;/p&gt;

&lt;p&gt;It also showed &lt;strong&gt;WEB-A&lt;/strong&gt; (YouTube iframe SOP) and &lt;strong&gt;WEB-9&lt;/strong&gt; (Zalo WebView inject). I did not patch those.&lt;/p&gt;

&lt;p&gt;That was the useful part.&lt;/p&gt;

&lt;p&gt;In the Finish-Up-A-Thon I learned not to ask AI to dump code. Same here. Gemini helped me triage. The shim, the error-boundary helper, the parse wrapper, and the tests are mine.&lt;/p&gt;

&lt;p&gt;Sentry showed the crash on a phone I do not own.&lt;/p&gt;

&lt;p&gt;Gemini helped me not fix the wrong crash.&lt;/p&gt;

&lt;p&gt;Then I made the smallest change I would still trust on a weekend night.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>sentry</category>
      <category>gemini</category>
    </item>
    <item>
      <title>I Rebuilt My Karaoke App So Everyone's Phone Could Be a Remote</title>
      <dc:creator>Lê Huy Giang</dc:creator>
      <pubDate>Mon, 01 Jun 2026 00:47:27 +0000</pubDate>
      <link>https://dev.to/lehuygiang28/i-rebuilt-my-karaoke-app-so-everyones-phone-could-be-a-remote-4k8b</link>
      <guid>https://dev.to/lehuygiang28/i-rebuilt-my-karaoke-app-so-everyones-phone-could-be-a-remote-4k8b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;VKara&lt;/strong&gt; is a browser-based karaoke room app for singing at home with friends or family.&lt;/p&gt;

&lt;p&gt;It is not trying to replace YouTube.&lt;/p&gt;

&lt;p&gt;YouTube is already great at playing videos. It already has almost every karaoke song we need. But YouTube is not really designed to manage a karaoke night where many people want to choose songs together.&lt;/p&gt;

&lt;p&gt;That is the gap VKara tries to fill.&lt;/p&gt;

&lt;p&gt;You open VKara on a TV or laptop as the main playback screen. Everyone else joins the same room from their phone using a 4-digit room code or QR code. Then anyone can search for songs, add them to the queue, pause, resume, or skip.&lt;/p&gt;

&lt;p&gt;The TV only needs to play the video.&lt;/p&gt;

&lt;p&gt;Everyone's phone becomes their own remote.&lt;/p&gt;

&lt;p&gt;That is the whole idea.&lt;/p&gt;

&lt;p&gt;Simple enough to explain in one sentence. Not simple enough to build in one weekend. I learned that part the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://vkara.vercel.app/en" rel="noopener noreferrer"&gt;https://vkara.vercel.app/en&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub repo: &lt;a href="https://github.com/lehuygiang28/vkara" rel="noopener noreferrer"&gt;https://github.com/lehuygiang28/vkara&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Before branch: &lt;a href="https://github.com/lehuygiang28/vkara/tree/before" rel="noopener noreferrer"&gt;https://github.com/lehuygiang28/vkara/tree/before&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Old backend repo: &lt;a href="https://github.com/lehuygiang28/vkara-api" rel="noopener noreferrer"&gt;https://github.com/lehuygiang28/vkara-api&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small warning: the demo is running on limited resources, so if it is slow, please give it a moment. My wallet is still a student wallet. lol.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open VKara on a TV or laptop.&lt;/li&gt;
&lt;li&gt;Join the room from a phone by code or QR.&lt;/li&gt;
&lt;li&gt;Search for a karaoke video.&lt;/li&gt;
&lt;li&gt;Add it to the shared queue.&lt;/li&gt;
&lt;li&gt;Control playback together.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyl1x2rzax072ohmk1pce.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyl1x2rzax072ohmk1pce.jpg" alt="Old VKara version: desktop, mobile search, and mobile controls" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before: the idea worked, but the product still felt like a video app squeezed into a karaoke use case.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzxg3rtr5k8vo8caokxml.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzxg3rtr5k8vo8caokxml.jpg" alt="Current VKara mobile flow" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After: the mobile flow is now focused on joining, searching, choosing an action, and controlling playback.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;Before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend and backend lived in separate repos&lt;/li&gt;
&lt;li&gt;Mobile UI was basically a compressed desktop UI&lt;/li&gt;
&lt;li&gt;WebSocket contracts were spread across projects&lt;/li&gt;
&lt;li&gt;The TV screen had too many controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Merged everything into a Bun workspace monorepo&lt;/li&gt;
&lt;li&gt;Moved shared API/WebSocket contracts into packages/shared-types&lt;/li&gt;
&lt;li&gt;Rebuilt the product around two roles: TV as screen, phone as remote&lt;/li&gt;
&lt;li&gt;Simplified the mobile flow around join, search, queue, and controls&lt;/li&gt;
&lt;li&gt;Improved room state, reconnect behavior, playback sync, and deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;I started VKara around early 2025.&lt;/p&gt;

&lt;p&gt;At that time, my goal was very personal. I wanted a better way to sing karaoke at home with friends. The normal setup was: open YouTube on a TV, search for karaoke videos, and pass control around.&lt;/p&gt;

&lt;p&gt;It worked, but it was awkward.&lt;/p&gt;

&lt;p&gt;One person was searching. Another person accidentally played a video immediately. Someone else wanted to know what song was next. The queue was unclear. Casting also usually depended on everyone being on the same Wi-Fi network.&lt;/p&gt;

&lt;p&gt;So I built a prototype.&lt;/p&gt;

&lt;p&gt;The first version technically worked. And by "worked", I mean the dangerous kind of worked: it worked enough to prove the idea, but not enough to confidently bring it to a real karaoke night.&lt;/p&gt;

&lt;p&gt;The product had problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the desktop screen had too many controls around the video&lt;/li&gt;
&lt;li&gt;the mobile UI felt like a compressed desktop app&lt;/li&gt;
&lt;li&gt;search, queue, and controls were too close together&lt;/li&gt;
&lt;li&gt;realtime state was fragile&lt;/li&gt;
&lt;li&gt;the frontend and backend lived in separate repos&lt;/li&gt;
&lt;li&gt;shared types and WebSocket contracts were not cleanly owned by one place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part mattered more than I expected.&lt;/p&gt;

&lt;p&gt;VKara is a realtime app. The frontend and backend have to agree on room state, queue state, video metadata, player commands, and WebSocket messages. When those contracts live in different places, every change feels risky.&lt;/p&gt;

&lt;p&gt;So I stopped working on it.&lt;/p&gt;

&lt;p&gt;Not because the idea was bad.&lt;/p&gt;

&lt;p&gt;Because the project was hard to change.&lt;/p&gt;

&lt;p&gt;When I came back this year, I decided not to start with new features. I started by making the project safe to change.&lt;/p&gt;

&lt;p&gt;The first major step was merging the old frontend repo and old backend repo into one Bun workspace monorepo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web                # Next.js frontend
apps/api                # Elysia + WebSocket backend
packages/shared-types   # shared API/WebSocket contracts
packages/shared-utils   # shared helper functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was not just folder cleanup. It changed how I could work on the project.&lt;/p&gt;

&lt;p&gt;After the monorepo migration, web and API changes could be checked together. Shared contracts could live in &lt;code&gt;packages/shared-types&lt;/code&gt;. I could change a room event or video shape without hunting through two repos and hoping I remembered everything.&lt;/p&gt;

&lt;p&gt;The second major change was UX.&lt;/p&gt;

&lt;p&gt;At first, I treated VKara like a responsive website:&lt;/p&gt;

&lt;p&gt;Desktop layout. Mobile layout. Done.&lt;/p&gt;

&lt;p&gt;That was the wrong mental model.&lt;/p&gt;

&lt;p&gt;A TV is not just a large phone. A phone is not just a small TV.&lt;/p&gt;

&lt;p&gt;They have different jobs.&lt;/p&gt;

&lt;p&gt;So VKara now thinks in two roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TV / laptop:&lt;/strong&gt; the playback screen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone:&lt;/strong&gt; the remote&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That one decision made the product much clearer.&lt;/p&gt;

&lt;p&gt;The TV should stay clean. It should show the video, the room code or QR, and the basic room controls.&lt;/p&gt;

&lt;p&gt;The phone should handle joining, searching, adding to queue, and playback actions.&lt;/p&gt;

&lt;p&gt;The real flow became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Join a room → search a song → add it to the queue → keep singing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A lot of the comeback work was not glamorous. It was the kind of work users only notice when it fails: WebSocket room state, reconnect behavior, playback sync, queue updates, room cleanup, YouTube metadata shape, Docker builds, GitHub Actions, and Vercel deployment after the monorepo migration.&lt;/p&gt;

&lt;p&gt;Nobody at a karaoke night cares how clever the architecture is.&lt;/p&gt;

&lt;p&gt;They only care that when they tap a button, the room responds.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;The biggest difference between the first version and this comeback was not only GitHub Copilot.&lt;/p&gt;

&lt;p&gt;It was how I used it.&lt;/p&gt;

&lt;p&gt;In the first version, my AI-assisted workflow was too naive. I asked for code, patched bugs by hand, and moved fast. That helped me build a prototype, but it did not always leave behind code I trusted.&lt;/p&gt;

&lt;p&gt;This time, I used GitHub Copilot more like a finishing partner.&lt;/p&gt;

&lt;p&gt;Before changing code, I used Ask Mode to understand the project again. VKara had been sitting for a long time, and honestly, I did not remember every decision I made.&lt;/p&gt;

&lt;p&gt;One of my first questions was basically:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Why am I not using SWR or React Query in this frontend? What am I actually using instead?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer reminded me that the app was using Zustand stores for client state, direct fetch calls for server data, and WebSocket-driven realtime sync for room/player state.&lt;/p&gt;

&lt;p&gt;That question saved me from changing the wrong layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnk97nqpnlul0eeea4lo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnk97nqpnlul0eeea4lo.png" alt="Using Ask Mode in VS Code to understand the old frontend data flow" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ask Mode helped me understand the old data flow before editing code.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For larger work, I used Plan Mode before execution.&lt;/p&gt;

&lt;p&gt;Instead of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Make these two projects cleaner."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I asked for a real migration plan:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Merge &lt;code&gt;vkara&lt;/code&gt; and &lt;code&gt;vkara-api&lt;/code&gt; into one monorepo, keep history, use Bun workspace, find shared types, and move contracts into shared packages."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fixtml050mv2il2pyp8c9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fixtml050mv2il2pyp8c9.png" alt="Using Plan Mode in VS Code for the monorepo migration" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Plan Mode turned a vague refactor into a safer migration path.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That became my loop:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Think → Ask → Plan → Execute → Verify → Repeat&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For bugs, I pasted exact TypeScript errors or Docker logs.&lt;/p&gt;

&lt;p&gt;For UI work, I used screenshots and described what felt wrong.&lt;/p&gt;

&lt;p&gt;For architecture questions, I asked for alternatives before touching code.&lt;/p&gt;

&lt;p&gt;That was the real upgrade.&lt;/p&gt;

&lt;p&gt;In the first version, I mostly asked AI to generate code.&lt;/p&gt;

&lt;p&gt;In the comeback, I asked GitHub Copilot to help me investigate, compare, plan, refactor, verify, and explain trade-offs.&lt;/p&gt;

&lt;p&gt;The final result is still not perfect.&lt;/p&gt;

&lt;p&gt;But now I know what actually matters.&lt;/p&gt;

&lt;p&gt;Not “AI lyrics” or some huge feature that sounds cool in a roadmap.&lt;/p&gt;

&lt;p&gt;The real problems are smaller and more human: who is allowed to skip, who added this song, whose turn is next, how to stop one person from filling the whole queue, how to rejoin quickly when someone closes their phone, and how to bring back the same queue for the next karaoke night.&lt;/p&gt;

&lt;p&gt;YouTube already gives us the videos.&lt;/p&gt;

&lt;p&gt;VKara should make the room feel calm.&lt;/p&gt;

&lt;p&gt;But VKara is no longer just a prototype I keep for myself.&lt;/p&gt;

&lt;p&gt;It is something I can open on a TV, hand my phone to a friend, and say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Pick the next song."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And honestly, that already feels like a win.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>githubcopilot</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Full-Stack Proxy with a Dashboard to Scale the Gemini API. Stop Juggling Keys</title>
      <dc:creator>Lê Huy Giang</dc:creator>
      <pubDate>Tue, 30 Sep 2025 16:22:12 +0000</pubDate>
      <link>https://dev.to/lehuygiang28/i-built-a-full-stack-proxy-with-a-dashboard-to-scale-the-gemini-api-stop-juggling-keys-3a3l</link>
      <guid>https://dev.to/lehuygiang28/i-built-a-full-stack-proxy-with-a-dashboard-to-scale-the-gemini-api-stop-juggling-keys-3a3l</guid>
      <description>&lt;p&gt;Hey Dev Community! 👋&lt;/p&gt;

&lt;p&gt;If you're like me, you love hacking on projects with the Google Gemini API. But you also know that feeling... you're in the zone, code is flowing, and &lt;strong&gt;BAM! Rate limit exceeded.&lt;/strong&gt; 😭&lt;/p&gt;

&lt;p&gt;The free tier is fantastic, but that RPM/RPD limit can be a real creativity killer.&lt;/p&gt;

&lt;p&gt;This exact problem bugged me so much that I decided to build my first major open-source project to solve it. I'm super excited (and a little nervous!) to share it with you all: &lt;a href="https://github.com/lehuygiang28/gemini-proxy" rel="noopener noreferrer"&gt;&lt;strong&gt;Gemini Proxy&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚀 This project is fresh off the press and actively being developed!&lt;/strong&gt; I'm constantly adding new features, fixing bugs, and improving the developer experience. Your feedback and contributions are not just welcome—they're essential to making this tool amazing for the entire community.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ What's in the Box?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🔑 Stop Juggling API Keys&lt;/strong&gt; → Throw all your Google Gemini keys into one secure spot (a Supabase DB). The dashboard lets you add, toggle, or remove them on the fly. No more .env file madness!
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚖️ Automatic Load Balancing&lt;/strong&gt; → It smartly spreads your requests across all your active keys. If one key gets tired and hits its limit, the proxy just grabs the next one. Zero downtime for your app.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🛡️ Your Own Secret Handshake&lt;/strong&gt; → Create special &lt;em&gt;proxy&lt;/em&gt; keys for your apps. This means your real Google API keys never leave the backend. Super secure!
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📊 Know What's Happening&lt;/strong&gt; → A built-in dashboard shows you logs, performance, and usage in real-time. No more flying blind!
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌍 Deploy It Your Way&lt;/strong&gt; → The full-stack Next.js app is the easiest way to get everything, but if you just need the core logic, you can deploy it to Vercel Edge, Cloudflare Workers, and more.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🏗️ How It Works
&lt;/h2&gt;

&lt;p&gt;The concept is simple but powerful. Your application makes requests to your deployed Gemini Proxy instance using a proxy key. The proxy then uses Supabase as its "brain" to fetch an available Google Gemini API key, forward the request, and log the transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your App → Gemini Proxy → Google Gemini API  
               ↓  
            Supabase (for keys, logs, and metrics)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 Get Up &amp;amp; Running in 5 Minutes
&lt;/h2&gt;

&lt;p&gt;I wanted the setup to be as painless as possible. You can get the full experience (dashboard included!) up and running in about 5 minutes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prep Supabase&lt;/strong&gt; → You'll need a Supabase project to act as the brain. I made an &lt;a href="https://github.com/lehuygiang28/gemini-proxy/blob/main/docs/getting-started.md" rel="noopener noreferrer"&gt;&lt;strong&gt;Illustrated Setup Guide&lt;/strong&gt;&lt;/a&gt; and a &lt;a href="https://github.com/lehuygiang28/gemini-proxy/blob/main/docs/quick-usage.md" rel="noopener noreferrer"&gt;&lt;strong&gt;Quick Usage&lt;/strong&gt;&lt;/a&gt; with pictures and everything to walk you through it.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-Click Deploy to Vercel&lt;/strong&gt; → Just hit this magic button on &lt;a href="https://github.com/lehuygiang28/gemini-proxy/tree/main?tab=readme-ov-file#alternative-deploy-to-vercel-recommended-for-production" rel="noopener noreferrer"&gt;main readme file&lt;/a&gt;. It'll clone the repo and get you ready to go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect to Supabase&lt;/strong&gt; → Vercel will ask for your Supabase URL and key. Just paste 'em in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch!&lt;/strong&gt; → You're live! You have a running instance with a dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feed It Some Keys&lt;/strong&gt; → Now for the fun part. Use the CLI or the shiny new web UI to add your first proxy key and all your Google Gemini API keys.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  💻 Show Me the Code
&lt;/h2&gt;

&lt;p&gt;Okay, you're deployed. Now what? Just point your favorite AI library to your new proxy URL. Here's how it looks with the Official Google Genai. Check out how clean this is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleGenAI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@google/genai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;genAI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_PROXY_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;httpOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:9090/api/gproxy/gemini&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Generate content&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;genAI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-2.0-flash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Count from 1 to 10&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And boom! Your app is now supercharged, running through a scalable, managed proxy that you control.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔥 This Project Needs YOUR Help
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;🔥 This project is in active development and I need YOUR help!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Honestly, this is my first big open-source project, and I'm really passionate about making it something the community finds useful. For now, my main collaborator has been AI (it's been an interesting journey, ask me about it sometime!), but I know that the best projects are built with people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's where you come in - and I genuinely appreciate every contribution!&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🐛 Found a bug?&lt;/strong&gt; → &lt;a href="https://github.com/lehuygiang28/gemini-proxy/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; - Please report it! I'm actively fixing issues and your bug reports help me make this tool rock-solid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💡 Got an idea?&lt;/strong&gt; → &lt;a href="https://github.com/lehuygiang28/gemini-proxy/discussions" rel="noopener noreferrer"&gt;Start a discussion&lt;/a&gt; - I'm all ears! Feature requests, improvements, or even just suggestions for better UX are incredibly valuable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;👩‍💻 Want to contribute code?&lt;/strong&gt; → &lt;a href="https://github.com/lehuygiang28/gemini-proxy/fork" rel="noopener noreferrer"&gt;Fork the repo&lt;/a&gt; - Pull requests are always welcome! Whether it's fixing a typo, adding a feature, or improving performance - every contribution matters and I'll make sure to acknowledge your hard work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📝 Questions about the code?&lt;/strong&gt; → Don't hesitate to ask! I'm here to learn and grow with all of you, and your questions help me improve both the code and the documentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm looking for feedback, ideas, and maybe even a few brave souls who want to contribute. If you read my code and have questions or see a better way to do something, please tell me! I'm here to learn and grow with all of you.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Check out the Repo: &lt;a href="https://github.com/lehuygiang28/gemini-proxy" rel="noopener noreferrer"&gt;lehuygiang28/gemini-proxy&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you think this is a cool idea, please &lt;strong&gt;give it a ⭐️ on GitHub!&lt;/strong&gt; It would seriously make my day.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is my first major open-source project, and I'm committed to making it amazing with your help. Let's build something great together!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>opensource</category>
      <category>api</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
