<?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: Vishal</title>
    <description>The latest articles on DEV Community by Vishal (@statsinsta).</description>
    <link>https://dev.to/statsinsta</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%2F4185633%2Fc2245336-6572-44c0-97e6-d9251104087a.png</url>
      <title>DEV Community: Vishal</title>
      <link>https://dev.to/statsinsta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/statsinsta"/>
    <language>en</language>
    <item>
      <title>My Instagram tool's traffic was 82% Indonesian — so I taught it WhatsApp</title>
      <dc:creator>Vishal</dc:creator>
      <pubDate>Fri, 23 Oct 2026 10:19:20 +0000</pubDate>
      <link>https://dev.to/statsinsta/my-instagram-tools-traffic-was-82-indonesian-so-i-taught-it-whatsapp-542e</link>
      <guid>https://dev.to/statsinsta/my-instagram-tools-traffic-was-82-indonesian-so-i-taught-it-whatsapp-542e</guid>
      <description>&lt;h2&gt;
  
  
  The Search Console export that rewrote my roadmap
&lt;/h2&gt;

&lt;p&gt;Stats Insta is a small tool I built for myself and then put online: upload an Instagram chat export, get statistics about your conversations — who texts most, when you're active, your top emojis, Wrapped-style story cards you can share. Everything runs in the browser; nothing is uploaded anywhere.&lt;/p&gt;

&lt;p&gt;A few weeks ago I finally did the boring thing and exported sixteen months of Google Search Console data to actually look at it. Around 6,500 clicks. And &lt;strong&gt;82% of them from Indonesia.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It gets stranger. Most of my top search queries weren't keywords at all — they were people typing my &lt;em&gt;old&lt;/em&gt; Vercel domain name into Google to find their way back. Traffic had peaked back in December 2025 (Wrapped season, of course) and was slowly sliding. In other words: the tool had found an audience almost by accident, the audience was Indonesian, and it was loyal enough to Google a URL.&lt;/p&gt;

&lt;p&gt;Here's the part that stung: Indonesia is a WhatsApp-first country. For most of my actual users, WhatsApp is not "an app" — it's the default layer for talking to everyone. And my tool supported exactly zero WhatsApp. Every one of those users was analyzing the chats they have on their &lt;em&gt;secondary&lt;/em&gt; app, because the app they actually live in wasn't supported at all.&lt;/p&gt;

&lt;p&gt;So the roadmap rewrote itself. Before any new features: teach the tool WhatsApp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two parsers, two completely different worlds
&lt;/h2&gt;

&lt;p&gt;Instagram exports are a gift. You request your data, wait, and get &lt;code&gt;message_1.json&lt;/code&gt; — real structured JSON with &lt;code&gt;sender_name&lt;/code&gt;, &lt;code&gt;timestamp_ms&lt;/code&gt;, a &lt;code&gt;reactions&lt;/code&gt; array, the works. Parsing it is an afternoon.&lt;/p&gt;

&lt;p&gt;A WhatsApp export is a &lt;code&gt;.txt&lt;/code&gt; file that looks like a chat transcript, because it is one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;14/08/2025, 21:03 - Messages are end-to-end encrypted
14/08/2025, 21:04 - Priya: ok so about tomorrow
[14/08/2025, 21:04:37] Arjun: wait, which tomorrow?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That first line is Android format, the third is iOS. Neither is a standard — the comma is optional, seconds come and go, and the clock is 12-hour in some regions and 24-hour in others. The parser ended up starting with two regexes just to recognize a line:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IOS_LINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\[(\d{1,2})[&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;/](\d{1,2})[&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;/](\d{2,4})&lt;/span&gt;&lt;span class="sr"&gt;,&lt;/span&gt;&lt;span class="se"&gt;?\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(\d{1,2})&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;(\d{2})(?:&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;(\d{2}))?\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;AaPp&lt;/span&gt;&lt;span class="se"&gt;]\.?[&lt;/span&gt;&lt;span class="sr"&gt;Mm&lt;/span&gt;&lt;span class="se"&gt;]\.?)?\]\s?(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&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;ANDROID_LINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(\d{1,2})[&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;/](\d{1,2})[&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;/](\d{2,4})&lt;/span&gt;&lt;span class="sr"&gt;,&lt;/span&gt;&lt;span class="se"&gt;?\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(\d{1,2})&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;(\d{2})(?:&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;(\d{2}))?\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;AaPp&lt;/span&gt;&lt;span class="se"&gt;]\.?[&lt;/span&gt;&lt;span class="sr"&gt;Mm&lt;/span&gt;&lt;span class="se"&gt;]\.?)?\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;2010-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;2015&lt;/span&gt;&lt;span class="se"&gt;]\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the real problems start:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Half the lines aren't messages.&lt;/strong&gt; The encryption notice, "Priya added Arjun", subject changes, "security code changed", missed calls — all of it sits in the file wearing a timestamp, and all of it has to be excluded or your statistics count WhatsApp itself as a very talkative participant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some system lines disguise themselves as messages.&lt;/strong&gt; &lt;code&gt;Alice changed the subject to "Trip: Friday plans"&lt;/code&gt; contains a &lt;code&gt;": "&lt;/code&gt; — so a naive split happily parses it as a message from a person named &lt;em&gt;"Alice changed the subject to "Trip"&lt;/em&gt;. I ended up treating a "sender" as fake if it's implausibly long or contains a system verb before the colon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-line messages have no prefix at all.&lt;/strong&gt; If someone sends a message with line breaks, the continuation lines arrive naked. The rule is simple — a line with no timestamp belongs to the previous message — but you only discover that after your message count doesn't match the chat you exported by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The file is full of invisible characters.&lt;/strong&gt; A BOM at the start, narrow no-break spaces before AM/PM, left-to-right marks wrapped around lines. Normalize them away first, or your regexes fail on files that &lt;em&gt;look&lt;/em&gt; identical to the ones that work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the worst one: nobody tells you the date order.&lt;/strong&gt; Is &lt;code&gt;03/04/2025&lt;/code&gt; the 3rd of April or March 4th? WhatsApp doesn't label it, and guessing wrong swaps days and months across the entire history. The parser scans the whole file before parsing anything:&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;dayFirst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sawMonthFirstEvidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &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;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lines&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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matchTimestampLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &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;first&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dayFirst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// 13+ can't be a month&lt;/span&gt;
  &lt;span class="k"&gt;if &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;second&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;sawMonthFirstEvidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sawMonthFirstEvidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;dayFirst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any date's first number is above 12, it &lt;em&gt;must&lt;/em&gt; be the day. If only the second number ever goes above 12, the file is month-first. If the file never gives unambiguous evidence — a short chat from the first twelve days of a month — it defaults to day-first, which matches the export locales of most of my users: Indonesia, India, Europe, Latin America. It's a heuristic, and I wrote it down as one in the code comments, because pretending it's a guarantee would be lying.&lt;/p&gt;

&lt;h2&gt;
  
  
  One stats engine, two front doors
&lt;/h2&gt;

&lt;p&gt;The best decision in the whole project was refusing to build a second analyzer. The WhatsApp parser's only job is to normalize a messy text file into the exact same shape the Instagram path already produces:&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="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sender_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;Message&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;Because the model is identical, every existing statistic, chart and story card — totals, distribution, the activity heatmap, night owls, conversation starters — worked on WhatsApp chats the moment the parser did. Zero changes downstream.&lt;/p&gt;

&lt;p&gt;The only additions were three optional flags on &lt;code&gt;Message&lt;/code&gt;, and they turned out to be where the fun was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;isDeleted&lt;/code&gt;&lt;/strong&gt; — "This message was deleted" arrives &lt;em&gt;under the sender's name&lt;/em&gt;, so instead of discarding it, the parser keeps it as a flagged, content-less message. It counts toward that person's totals but is invisible to text statistics. That's what powers a Deleted Messages leaderboard — who in your group chat deletes the most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;isMedia&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;&amp;lt;Media omitted&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;attached: IMG-2041.jpg&amp;gt;&lt;/code&gt;, &lt;code&gt;IMG-2041.jpg (file attached)&lt;/code&gt; (captions kept, when there are any). Powers Media Shared counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;isEdited&lt;/code&gt;&lt;/strong&gt; — WhatsApp appends &lt;code&gt;&amp;lt;This message was edited&amp;gt;&lt;/code&gt; to edited messages, sometimes on its own line. The parser strips the marker and flags the message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From those flags plus the plain text fell out a set of stats that make no sense for Instagram but are pure WhatsApp: &lt;strong&gt;Top Link Sharers, Questions Asked&lt;/strong&gt; (who asks the most questions), &lt;strong&gt;Biggest Double-Texter&lt;/strong&gt;, plus &lt;strong&gt;Signature Emoji&lt;/strong&gt;, &lt;strong&gt;Fastest Responder&lt;/strong&gt; (average reply gaps) and &lt;strong&gt;Morning Person&lt;/strong&gt;. One graceful accident: WhatsApp exports carry no reaction data, so reaction-based stats simply find nothing — exactly what they already did for an Instagram chat where nobody reacted. Nothing needed special-casing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;The tool now takes Instagram JSON and WhatsApp &lt;code&gt;.txt&lt;/code&gt;/ZIP exports through the same upload box, tells them apart on its own, and runs the same engine on both. There's a dedicated WhatsApp page, WhatsApp-specific cards, a downloadable report, and Wrapped story cards that now say "WhatsApp" when they're about a WhatsApp chat (a detail I got wrong in the first version, and a user caught within hours — platform support means the &lt;em&gt;copy&lt;/em&gt;, not just the parser).&lt;/p&gt;

&lt;p&gt;Is it too early for a victory lap? Completely. The honest result so far is: the tool finally supports the app its audience actually uses. Whether the December slide reverses is a question the next Search Console export will answer, and I've learned my lesson about reading those more often.&lt;/p&gt;

&lt;p&gt;If you want to try it — everything runs locally in your browser, your chats never leave your device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The tool:&lt;/strong&gt; &lt;a href="https://stats-insta.vercel.app" rel="noopener noreferrer"&gt;https://stats-insta.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp chat analyzer:&lt;/strong&gt; &lt;a href="https://stats-insta.vercel.app/whatsapp-chat-analyzer" rel="noopener noreferrer"&gt;https://stats-insta.vercel.app/whatsapp-chat-analyzer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guide — how to export a WhatsApp chat:&lt;/strong&gt; &lt;a href="https://stats-insta.vercel.app/guides/export-whatsapp-chat" rel="noopener noreferrer"&gt;https://stats-insta.vercel.app/guides/export-whatsapp-chat&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you've ever parsed WhatsApp exports yourself and fought the date-order problem differently, I genuinely want to hear how. The comments are open.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Built a 100% In-Browser Instagram Chat Analyzer (Zero Cloud Uploads)</title>
      <dc:creator>Vishal</dc:creator>
      <pubDate>Wed, 14 Oct 2026 23:30:17 +0000</pubDate>
      <link>https://dev.to/statsinsta/how-i-built-a-100-in-browser-instagram-chat-analyzer-zero-cloud-uploads-1odf</link>
      <guid>https://dev.to/statsinsta/how-i-built-a-100-in-browser-instagram-chat-analyzer-zero-cloud-uploads-1odf</guid>
      <description>&lt;p&gt;Direct messages contain our most intimate conversations, jokes, memories, and photos. Yet, when searching for tools to visualize chat history or generate Spotify Wrapped-style stats for group chats, virtually every existing tool asks for your Instagram login or forces you to upload your sensitive ZIP archives to their cloud servers.&lt;/p&gt;

&lt;p&gt;I wanted to change that. I built &lt;strong&gt;&lt;a href="https://stats-insta.vercel.app/" rel="noopener noreferrer"&gt;Stats Insta&lt;/a&gt;&lt;/strong&gt; — a completely free, 100% client-side Instagram Chat Analyzer where your data never leaves your device.&lt;/p&gt;

&lt;p&gt;In this article, I will share the engineering challenges behind parsing multi-megabyte JSON exports in the browser, decoding escaped Unicode emojis, computing deep conversational metrics, and generating 9:16 Instagram Story cards directly from the DOM.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔒 The Architecture: 100% Client-Side Privacy
&lt;/h2&gt;

&lt;p&gt;The core philosophy behind &lt;strong&gt;&lt;a href="https://stats-insta.vercel.app/" rel="noopener noreferrer"&gt;Stats Insta&lt;/a&gt;&lt;/strong&gt; is &lt;strong&gt;Local-First Processing&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero Server Uploads:&lt;/strong&gt; The application does not have a backend parsing server or database for chat messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native HTML5 FileReader:&lt;/strong&gt; When a user drops a &lt;code&gt;message_1.json&lt;/code&gt; file, the browser reads the file directly into client-side RAM using &lt;code&gt;FileReader.readAsText()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session-Only Memory:&lt;/strong&gt; Once you refresh or close the browser tab, the data is instantly garbage-collected by the browser engine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can verify this in Chrome DevTools (&lt;strong&gt;F12 &amp;gt; Network Tab&lt;/strong&gt;): dropping a 50MB conversation file triggers &lt;strong&gt;0 network POST requests&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Technical Challenge 1: Decoding Instagram's Unicode Escapes
&lt;/h2&gt;

&lt;p&gt;When Meta exports your message history in JSON format, emojis and special characters are not stored as standard UTF-8 strings. Instead, they are encoded as escaped byte sequences (e.g., &lt;code&gt;\u00e2\u009d\u00a4&lt;/code&gt; for a red heart).&lt;/p&gt;

&lt;p&gt;If rendered directly in React, those characters appear garbled (mojibake). The subtle part is the input shape: by the time the file has gone through &lt;code&gt;JSON.parse&lt;/code&gt;, the strings are no longer literal &lt;code&gt;\uXXXX&lt;/code&gt; escape text. Each UTF-8 byte has become its own code point, so the red heart arrives as the three characters U+00E2, U+009D and U+00A4. The fix is to reinterpret those code points as UTF-8 bytes, which is exactly what the &lt;code&gt;utf8&lt;/code&gt; package does. The parser applies it to message content, sender names and reactions as each file is parsed:&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="nx"&gt;utf8&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&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;decodedContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&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;decodedSenderName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender_name&lt;/span&gt; &lt;span class="o"&gt;??&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;decodedReactions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reactions&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reaction&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actor&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;That turns the byte-sequence characters back into native emojis at parse time, without lagging the main thread.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Correction (October 2026): the first version of this section showed a different helper that rewrote &lt;code&gt;\uXXXX&lt;/code&gt; sequences to &lt;code&gt;%uXXXX&lt;/code&gt; and passed them to &lt;code&gt;decodeURIComponent&lt;/code&gt;. As a reader pointed out, &lt;code&gt;%u&lt;/code&gt; is not valid input for &lt;code&gt;decodeURIComponent&lt;/code&gt;, so that helper threw and returned its input unchanged - it never actually decoded anything. The &lt;code&gt;utf8.decode&lt;/code&gt; step above is what the shipped code has always used, and the dead helper has since been removed from the codebase.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Technical Challenge 2: Calculating Deep Conversational Metrics
&lt;/h2&gt;

&lt;p&gt;Simple message counts are boring. People care about the hidden dynamics of their friendships. We compute several unique behavioral metrics in browser memory:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The "Night Owls" Tracker (12:00 AM – 4:59 AM)
&lt;/h3&gt;

&lt;p&gt;We parse the Unix millisecond timestamps (&lt;code&gt;timestamp_ms&lt;/code&gt;) on each message and isolate activity occurring in the late-night window:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hour&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp_ms&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;nightOwlCounts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nightOwlCounts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;This immediately reveals who stays awake sending reels and memes while everyone else is asleep.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Conversation Starter Algorithm
&lt;/h3&gt;

&lt;p&gt;Who puts in the effort to keep the conversation alive? We sort the timeline chronologically and scan for silence gaps exceeding 12 hours:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TWELVE_HOURS_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastTimestamp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp_ms&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastTimestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;TWELVE_HOURS_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;starterCounts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;starterCounts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;lastTimestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp_ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Swear Jar
&lt;/h3&gt;

&lt;p&gt;We scan message strings against a local tokenized dictionary of 100+ global curse words and slang to generate a friendly group chat leaderboard of who has the dirtiest mouth.&lt;/p&gt;




&lt;h2&gt;
  
  
  📱 Technical Challenge 3: Generating 9:16 Instagram Story Wrapped Cards
&lt;/h2&gt;

&lt;p&gt;People love sharing aesthetic visual summaries with their friends. To make the tool virally shareable, I built an in-browser Story Card generator that renders a &lt;strong&gt;1080x1920 (9:16 Full HD)&lt;/strong&gt; graphic using HTML5 Canvas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Glassmorphism gradient backgrounds with radial glowing orbs&lt;/li&gt;
&lt;li&gt;Rounded progress bars showing conversational dominance percentages&lt;/li&gt;
&lt;li&gt;Dynamic badges and highlights&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1-Click Native Share&lt;/strong&gt;: On mobile devices, we invoke the &lt;strong&gt;Web Share API&lt;/strong&gt; (&lt;code&gt;navigator.share({ files: [pngFile] })&lt;/code&gt;), which opens the native iOS/Android share sheet directly into Instagram Stories!&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Try It Live &amp;amp; Explore the Code
&lt;/h2&gt;

&lt;p&gt;The project is completely free to use, and I have published detailed guides on understanding Meta's export architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Live Application:&lt;/strong&gt; &lt;a href="https://stats-insta.vercel.app/" rel="noopener noreferrer"&gt;https://stats-insta.vercel.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 &lt;strong&gt;Guide:&lt;/strong&gt; &lt;a href="https://stats-insta.vercel.app/guides/read-message-json" rel="noopener noreferrer"&gt;How to Open &amp;amp; Read Instagram message_1.json Files&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📙 &lt;strong&gt;Guide:&lt;/strong&gt; &lt;a href="https://stats-insta.vercel.app/guides/most-active-hours-instagram" rel="noopener noreferrer"&gt;Activity Heatmaps &amp;amp; Peak Texting Hours&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would love to hear feedback from the DEV community! What other client-side analytics or visual metrics would you like to see added?&lt;/p&gt;

&lt;h2&gt;
  
  
  Update - October 2026
&lt;/h2&gt;

&lt;p&gt;Stats Insta now works with WhatsApp chats as well as Instagram exports. The same in-browser analyzer can read a WhatsApp .txt or ZIP export and show message counts, activity patterns, media and deleted-message stats, link sharing, fastest responders, and Wrapped-style cards.&lt;/p&gt;

&lt;p&gt;New pages since this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WhatsApp chat analyzer: &lt;a href="https://stats-insta.vercel.app/whatsapp-chat-analyzer" rel="noopener noreferrer"&gt;https://stats-insta.vercel.app/whatsapp-chat-analyzer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Instagram Wrapped: &lt;a href="https://stats-insta.vercel.app/instagram-wrapped" rel="noopener noreferrer"&gt;https://stats-insta.vercel.app/instagram-wrapped&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How to export a WhatsApp chat: &lt;a href="https://stats-insta.vercel.app/guides/export-whatsapp-chat" rel="noopener noreferrer"&gt;https://stats-insta.vercel.app/guides/export-whatsapp-chat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;WhatsApp chat statistics guide: &lt;a href="https://stats-insta.vercel.app/guides/whatsapp-chat-statistics" rel="noopener noreferrer"&gt;https://stats-insta.vercel.app/guides/whatsapp-chat-statistics&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything still runs locally in your browser. Your chat files are not uploaded to a server.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
