-)}
-
-
diff --git a/src/content.config.ts b/src/content.config.ts
deleted file mode 100644
index 7bc4d301..00000000
--- a/src/content.config.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { defineCollection, reference } from "astro:content";
-import { z } from "astro/zod";
-import { glob } from "astro/loaders";
-
-const blog = defineCollection({
- loader: glob({ pattern: "**/[^_]*.md", base: "./src/content/blog" }),
- schema: ({ image }) =>
- z
- .object({
- title: z.string(),
- date: z.coerce.date().optional(),
- lastmod: z.coerce.date().optional(),
- draft: z.boolean().optional(),
- summary: z.string().optional(),
- images: z.array(image()).optional(),
- authors: z.array(reference("authors")).optional(),
- tags: z.array(z.string()).optional(),
- postLayout: z.string().optional(),
- canonicalUrl: z.string().nullable().optional(),
- })
- .passthrough(),
-});
-
-const authors = defineCollection({
- loader: glob({ pattern: "**/[^_]*.md", base: "./src/content/authors" }),
- schema: ({ image }) =>
- z
- .object({
- name: z.string(),
- avatar: z.union([image(), z.url()]).optional(),
- occupation: z.string().optional(),
- company: z.string().optional(),
- email: z.email().optional(),
- twitter: z.url().optional(),
- linkedin: z.url().optional(),
- github: z.url().optional(),
- stackoverflow: z.url().optional(),
- url: z.url().optional(),
- })
- .passthrough(),
-});
-
-export const collections = {
- blog,
- authors,
-};
diff --git a/src/content/authors/abhijeetprasad.md b/src/content/authors/abhijeetprasad.md
deleted file mode 100644
index e2ee039a..00000000
--- a/src/content/authors/abhijeetprasad.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Abhijeet Prasad
-avatar: ../../assets/avatars/default.png
-occupation: Senior Software Engineer
-twitter: https://twitter.com/imabhiprasad
-github: https://github.com/AbhiPrasad
----
-
-Senior Software Engineer working on Sentry’s JavaScript SDKs
diff --git a/src/content/authors/adammckerlie.md b/src/content/authors/adammckerlie.md
deleted file mode 100644
index c6f53e57..00000000
--- a/src/content/authors/adammckerlie.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Adam McKerlie
-avatar: ../../assets/avatars/adammckerlie.png
-occupation: Director of Engineering
-twitter: https://twitter.com/adammckerlie
-linkedin: https://www.linkedin.com/adammckerlie
-url: https://mckerlie.com
----
-
-Adam is a Director of Engineering at Sentry. He enjoys coding, eating and baking bread.
diff --git a/src/content/authors/andrewmcknight.md b/src/content/authors/andrewmcknight.md
deleted file mode 100644
index c8bd2a76..00000000
--- a/src/content/authors/andrewmcknight.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Andrew McKnight
-avatar: ../../assets/avatars/andrewmcknight.jpg
-occupation: Senior Software Engineer
-github: https://github.com/armcknight
-url: https://armcknight.com
----
diff --git a/src/content/authors/antonovchinnikov.md b/src/content/authors/antonovchinnikov.md
deleted file mode 100644
index 94f63e8a..00000000
--- a/src/content/authors/antonovchinnikov.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Anton Ovchinnikov
-avatar: ../../assets/avatars/antonovchinnikov.png
-occupation: Senior Software Engineer
-github: https://github.com/tonyo
-url: https://tonyo.info/
----
-
-Anton is a Senior Software Engineer at Sentry. During his tenure, Anton has worked on evolving and scaling the Sentry’s cloud infrastructure as an Operations Engineer, contributed to various Sentry SDKs, and led quality-related initiatives. Anton is curious about all things Cloud Native and Site Reliability, and gets excited every time he sees an alpaca.
diff --git a/src/content/authors/antonpirker.md b/src/content/authors/antonpirker.md
deleted file mode 100644
index dddded05..00000000
--- a/src/content/authors/antonpirker.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Anton Pirker
-avatar: ../../assets/avatars/antonpirker.png
-occupation: Senior Software Engineer
-url: https://anton-pirker.at/
-github: https://github.com/antonpirker/
-linkedin: https://www.linkedin.com/in/antonpirker/
----
-
-Anton is a software engineer from Vienna, Austria. Python makes him smile. Besides software he likes tinkering with and riding bikes.
diff --git a/src/content/authors/arminronacher.md b/src/content/authors/arminronacher.md
deleted file mode 100644
index fc03040f..00000000
--- a/src/content/authors/arminronacher.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Armin Ronacher
-avatar: ../../assets/avatars/arminronacher.png
-occupation: Principal Architect
-stackoverflow: https://stackoverflow.com/users/19990/armin-ronacher
-twitter: https://twitter.com/mitsuhiko
----
-
-Armin Ronacher is the Principal Architect at Sentry. He's the creator of the Flask web framework, very emotional about APIs and system architecture.
diff --git a/src/content/authors/arpadborsos.md b/src/content/authors/arpadborsos.md
deleted file mode 100644
index 12b72837..00000000
--- a/src/content/authors/arpadborsos.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Arpad Borsos
-avatar: ../../assets/avatars/arpadborsos.png
-occupation: Senior Software Engineer
-url: https://swatinem.de/
-github: https://github.com/Swatinem
----
-
-Arpad's interested in all things Rust. At Sentry he's responsible for the processing pipeline and Symbolicator. Outside of work, he maintains several open source projects and contributes to the Rust compiler ecosystem.
diff --git a/src/content/authors/ashanand.md b/src/content/authors/ashanand.md
deleted file mode 100644
index c0bf95d2..00000000
--- a/src/content/authors/ashanand.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-name: Ash Anand
-avatar: ../../assets/avatars/default.png
-occupation: Software Engineer
-github: https://github.com/0Calories
----
diff --git a/src/content/authors/benvinegar.md b/src/content/authors/benvinegar.md
deleted file mode 100644
index e3e24ff0..00000000
--- a/src/content/authors/benvinegar.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-name: Ben Vinegar
-avatar: ../../assets/avatars/benvinegar.png
-occupation: VP of Emerging Technologies
-twitter: https://twitter.com/bentlegen
-github: https://github.com/benvinegar
-linkedin: https://www.linkedin.com/in/benvinegar/
-url: https://benv.ca/
----
-
-Ben Vinegar is the VP Engineering at Sentry, an open source product that helps teams surface and fix production software issues. He's also the co-author of Third-party JavaScript, a contributor to O’Reilly’s Beautiful JavaScript, and an occasional conference speaker.
diff --git a/src/content/authors/billyvong.md b/src/content/authors/billyvong.md
deleted file mode 100644
index a62ea9b8..00000000
--- a/src/content/authors/billyvong.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Billy Vong
-avatar: ../../assets/avatars/billyvong.png
-occupation: Senior Software Engineer
-twitter: https://twitter.com/billyvg
-github: https://github.com/billyvg
----
diff --git a/src/content/authors/brunogarcia.md b/src/content/authors/brunogarcia.md
deleted file mode 100644
index 89de0a6e..00000000
--- a/src/content/authors/brunogarcia.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-name: Bruno Garcia
-avatar: ../../assets/avatars/brunogarcia.png
-occupation: Engineering Manager
-twitter: https://twitter.com/brungarc
-stackoverflow: https://stackoverflow.com/users/1977143/bruno-garcia
-github: https://github.com/bruno-garcia
-url: https://garcia.in
----
-
-A Software Engineer who is very intense about SDKs, world traveller and thinks home is wherever you set your beer down.
diff --git a/src/content/authors/cameroncooke.md b/src/content/authors/cameroncooke.md
deleted file mode 100644
index 1df5f734..00000000
--- a/src/content/authors/cameroncooke.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-name: Cameron Cooke
-avatar: ../../assets/avatars/cameroncooke.png
-occupation: Senior Software Engineer
-github: https://github.com/cameroncooke
-twitter: https://x.com/camsoft2000
-url: https://www.async-let.com
-linkedin: https://www.linkedin.com/in/cameroncooke1/
----
-
-Cameron Cooke is a Senior Software Engineer at Sentry, where he works on pre-production tooling as part of the Emerge Tools team. He is also the creator of [XcodeBuildMCP](https://github.com/cameroncooke/XcodeBuildMCP) and [AXe](https://github.com/cameroncooke/AXe), two widely used open-source projects that help developers leverage coding agents for iOS and macOS development.
diff --git a/src/content/authors/catherinelee.md b/src/content/authors/catherinelee.md
deleted file mode 100644
index c5fcd8ed..00000000
--- a/src/content/authors/catherinelee.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Catherine Lee
-avatar: ../../assets/avatars/catherinelee.jpg
-occupation: Software Engineer
-github: https://github.com/c298lee
-linkedin: https://www.linkedin.com/in/catherine-lee-uw/
----
diff --git a/src/content/authors/cathyteng.md b/src/content/authors/cathyteng.md
deleted file mode 100644
index f639ed23..00000000
--- a/src/content/authors/cathyteng.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Cathy Teng
-avatar: https://avatars.githubusercontent.com/u/70817427
-occupation: Software Engineer
-github: https://github.com/cathteng
-linkedin: https://www.linkedin.com/in/cathyteng/
----
diff --git a/src/content/authors/colinchartier.md b/src/content/authors/colinchartier.md
deleted file mode 100644
index 86e9fb2e..00000000
--- a/src/content/authors/colinchartier.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Colin Chartier
-avatar: ../../assets/avatars/colinchartier.jpeg
-occupation: Software
-github: https://github.com/colinchartier
-url: https://colinchartier.com
----
-
-Colin is a Software Developer on the Performance team at Sentry.
diff --git a/src/content/authors/coltonallen.md b/src/content/authors/coltonallen.md
deleted file mode 100644
index a3eb6e15..00000000
--- a/src/content/authors/coltonallen.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Colton Allen
-avatar: ../../assets/avatars/coltonallen.jpg
-occupation: Senior Software Engineer
-github: https://github.com/cmanallen
----
-
-Colton Allen is a Senior Software Engineer working on the Session Replay product. He's never met a computer he didn't like.
diff --git a/src/content/authors/default.md b/src/content/authors/default.md
deleted file mode 100644
index 0567b46a..00000000
--- a/src/content/authors/default.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Sentry Engineer
-avatar: ../../assets/avatars/default.png
-twitter: https://twitter.com/getsentry
-github: https://github.com/getsentry/sentry
----
-
-Sentry
diff --git a/src/content/authors/edwardgou.md b/src/content/authors/edwardgou.md
deleted file mode 100644
index 40ee9978..00000000
--- a/src/content/authors/edwardgou.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Edward Gou
-avatar: ../../assets/avatars/default.png
-occupation: Software Engineer
-github: https://github.com/edwardgou-sentry
----
-
-Software Engineer working on Sentry Performance.
diff --git a/src/content/authors/evanpurkhiser.md b/src/content/authors/evanpurkhiser.md
deleted file mode 100644
index 2e9d8aa3..00000000
--- a/src/content/authors/evanpurkhiser.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Evan Purkhiser
-avatar: ../../assets/avatars/evanpurkhiser.png
-occupation: Senior Software Engineer
-ur: https://evanpurkhiser.com/
-github: https://github.com/evanpurkhiser
----
-
-Evan Purkhiser is a software engineer who is passionate about building high quality systems, tools, and applications. He is continually striving to build something he's proud of, something he knows he didn’t take shortcuts on, and something worth sharing. He has a deep appreciation for modern design aesthetics and robust interaction design. The melding of form and function is his holy grail.
diff --git a/src/content/authors/filippopacifici.md b/src/content/authors/filippopacifici.md
deleted file mode 100644
index ef72cf64..00000000
--- a/src/content/authors/filippopacifici.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-name: Filippo Pacifici
-avatar: ../../assets/avatars/filippopacifici.png
-occupation: Staff Engineer
-twitter: https://twitter.com/filippopacifici
----
diff --git a/src/content/authors/francesconovy.md b/src/content/authors/francesconovy.md
deleted file mode 100644
index de52ed84..00000000
--- a/src/content/authors/francesconovy.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Francesco Novy
-avatar: ../../assets/avatars/francesconovy.jpeg
-occupation: Senior Software Engineer
-github: https://github.com/mydea
-url: https://fnovy.com/
----
-
-A software engineer with a passion for all things JavaScript, (board)games and travelling Europe by train.
diff --git a/src/content/authors/georgegritsouk.md b/src/content/authors/georgegritsouk.md
deleted file mode 100644
index c15e5147..00000000
--- a/src/content/authors/georgegritsouk.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: George Gritsouk
-avatar: ../../assets/avatars/georgegritsouk.png
-occupation: Senior Software Engineer
-github: https://github.com/gggritso/
-url: https://strict-machine.com/
----
-
-✌🏻
diff --git a/src/content/authors/hectordearman.md b/src/content/authors/hectordearman.md
deleted file mode 100644
index da19cbd3..00000000
--- a/src/content/authors/hectordearman.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Hector Dearman
-avatar: ../../assets/avatars/hectordearman.jpg
-occupation: Staff Software Engineer
-github: https://github.com/chromy
-url: https://tsundoku.io/
-linkedin: https://www.linkedin.com/in/hector-dearman/
----
diff --git a/src/content/authors/indragiekarunaratne.md b/src/content/authors/indragiekarunaratne.md
deleted file mode 100644
index 0cd05189..00000000
--- a/src/content/authors/indragiekarunaratne.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-name: Indragie Karunaratne
-avatar: ../../assets/avatars/indragiekarunaratne.png
-occupation: Director of Engineering
-twitter: https://twitter.com/indragie
-linkedin: https://linkedin.com/in/indragie/
-stackoverflow: https://stackoverflow.com/users/153112/indragie
-url: https://indragie.com/
----
-
-Director of Engineering at Sentry working on Profiling and Session Replay. Formerly CTO/co-founder of Specto, mobile infrastructure engineer at Meta, and independent macOS & iOS developer.
diff --git a/src/content/authors/ivanakellyer.md b/src/content/authors/ivanakellyer.md
deleted file mode 100644
index 161f56e0..00000000
--- a/src/content/authors/ivanakellyer.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Ivana Kellyer
-avatar: ../../assets/avatars/ivanakellyer.jpg
-occupation: Senior Software Engineer
-url: https://www.amarion.net/
-github: https://github.com/sentrivana/
-linkedin: https://www.linkedin.com/in/ivana-kellyer/
----
-
-SDK engineer. Owned by three cats. Weird code enthusiast.
diff --git a/src/content/authors/jamescrosswell.md b/src/content/authors/jamescrosswell.md
deleted file mode 100644
index 04442125..00000000
--- a/src/content/authors/jamescrosswell.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-name: James Crosswell
-avatar: ../../assets/avatars/jamescrosswell.jpg
-occupation: Software Engineering Contractor
-twitter: https://twitter.com/jamescrosswell
-stackoverflow: https://stackoverflow.com/users/1182461/james-crosswell
-github: https://github.com/jamescrosswell
-linkedin: https://www.linkedin.com/in/jamescrosswell/
----
-
-Technologist and adrenaline junkie.
diff --git a/src/content/authors/jamescunningham.md b/src/content/authors/jamescunningham.md
deleted file mode 100644
index a70a4d9c..00000000
--- a/src/content/authors/jamescunningham.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-name: James Cunningham
-avatar: ../../assets/avatars/james.png
-occupation: Engineer
-twitter: https://twitter.com/jtcunning
----
diff --git a/src/content/authors/janmichaelauer.md b/src/content/authors/janmichaelauer.md
deleted file mode 100644
index fb91bc99..00000000
--- a/src/content/authors/janmichaelauer.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Jan Michael Auer
-avatar: ../../assets/avatars/jan.png
-occupation: Staff Engineering
-twitter: https://twitter.com/jan_auer
-stackoverflow: https://stackoverflow.com/users/4228225/jan-michael-auer
-github: https://github.com/jan-auer
----
diff --git a/src/content/authors/kamilogorek.md b/src/content/authors/kamilogorek.md
deleted file mode 100644
index 29c9ff54..00000000
--- a/src/content/authors/kamilogorek.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Kamil Ogórek
-avatar: ../../assets/avatars/kamilogorek.png
-occupation: Senior Software Engineer
-twitter: https://twitter.com/kamilogorek
-stackoverflow: https://stackoverflow.com/users/1690906/kamil-og%c3%b3rek
-linkedin: https://linkedin.com/in/kamilogorek/
----
diff --git a/src/content/authors/katiebyers.md b/src/content/authors/katiebyers.md
deleted file mode 100644
index 5907466c..00000000
--- a/src/content/authors/katiebyers.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Katie Byers
-avatar: ../../assets/avatars/katiebyers.jpeg
-occupation: Software Engineer
-github: https://github.com/lobsterkatie
-linkedin: https://www.linkedin.com/in/byerskatie/
----
-
-Katie has been at Sentry since 2018, and has worked on the SDK and Issues teams. Based in San Francisco, but still a proud member of #redsoxnation. Listens to too many podcasts. Likes all dogs (and some people, too!).
diff --git a/src/content/authors/lazarnikolov.md b/src/content/authors/lazarnikolov.md
deleted file mode 100644
index fc748f12..00000000
--- a/src/content/authors/lazarnikolov.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Lazar Nikolov
-avatar: ../../assets/avatars/lazarnikolov.png
-occupation: Developer Advocate
-twitter: https://twitter.com/NikolovLazar
----
-
-Lazar Nikolov is a developer advocate with a passion for learning and teaching. He’s got a knack for anything UI: frontend frameworks, design, CSS. He’s a professional side-project starter, and amateur side-project finisher.
diff --git a/src/content/authors/leanderrodrigues.md b/src/content/authors/leanderrodrigues.md
deleted file mode 100644
index 2a7c28a4..00000000
--- a/src/content/authors/leanderrodrigues.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Leander Rodrigues
-avatar: https://avatars.githubusercontent.com/u/35509934
-occupation: software engineer
-github: https://github.com/leeandher/
-url: https://leander.xyz
----
diff --git a/src/content/authors/lucaforstner.md b/src/content/authors/lucaforstner.md
deleted file mode 100644
index 7d2977bb..00000000
--- a/src/content/authors/lucaforstner.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Luca Forstner
-avatar: ../../assets/avatars/default.png
-occupation: Software Engineer
-twitter: https://twitter.com/LucaForstner
-github: https://github.com/lforst
----
-
-Software Engineer working on Sentry’s JavaScript SDKs. It's fun.
diff --git a/src/content/authors/lukasstracke.md b/src/content/authors/lukasstracke.md
deleted file mode 100644
index 625c5a2d..00000000
--- a/src/content/authors/lukasstracke.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Lukas Stracke
-avatar: ../../assets/avatars/lukasstracke.jpg
-occupation: Software Engineer
-twitter: https://twitter.com/lukasstracke
-github: https://github.com/Lms24
-url: https://stracke.tech/
----
-
-Software Engineer working on Sentry’s JavaScript SDKs, flight sim nerd, guitar player and wannabe archer
diff --git a/src/content/authors/markstory.md b/src/content/authors/markstory.md
deleted file mode 100644
index ecc90bbf..00000000
--- a/src/content/authors/markstory.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-name: Mark Story
-avatar: ../../assets/avatars/markstory.png
-occupation: Staff Engineer
-stackoverflow: https://stackoverflow.com/users/186379/mark-story
-twitter: https://mastodon.social/@markstory
-github: https://github.com/markstory
-url: https://mark-story.com
----
-
-Mark is a staff-engineer at Sentry, and open source enthusiast. He's also built seven mechanical keyboards and counting!
diff --git a/src/content/authors/markushintersteiner.md b/src/content/authors/markushintersteiner.md
deleted file mode 100644
index ed10f414..00000000
--- a/src/content/authors/markushintersteiner.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Markus Hintersteiner
-avatar: ../../assets/avatars/markushintersteiner.jpg
-twitter: https://twitter.com/markushi_
-github: https://github.com/markushi
-url: https://androiddev.social/@markushi
----
-
-Still can’t decide on a favourite programming language. Alpine adventurer and potato lover.
diff --git a/src/content/authors/mikeihbe.md b/src/content/authors/mikeihbe.md
deleted file mode 100644
index a556a433..00000000
--- a/src/content/authors/mikeihbe.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Mike Ihbe
-avatar: ../../assets/avatars/mikeihbe.png
-occupation: Director of Engineering
-twitter: https://twitter.com/mikeihbe
-github: https://github.com/mikejihbe
-linkedin: https://www.linkedin.com/in/mike-ihbe/
----
-
-Mike Ihbe is a Directory of Engineering at Sentry, an open source product that helps teams surface and fix production software issues.
diff --git a/src/content/authors/nicholasdeschenes.md b/src/content/authors/nicholasdeschenes.md
deleted file mode 100644
index ec6e30a1..00000000
--- a/src/content/authors/nicholasdeschenes.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Nicholas Deschenes
-avatar: ../../assets/avatars/nicholasdeschenes.png
-occupation: Software Engineer
-twitter: https://twitter.com/_Idez_
-github: https://github.com/nsdeschenes
----
-
-Software Engineer working on Codecov’s Frontend
diff --git a/src/content/authors/noahmartin.md b/src/content/authors/noahmartin.md
deleted file mode 100644
index bc0f5179..00000000
--- a/src/content/authors/noahmartin.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Noah Martin
-avatar: ../../assets/avatars/noahmartin.jpg
-occupation: Director of Engineering
-github: https://github.com/noahsmartin
-url: https://noahmart.in
-linkedin: https://www.linkedin.com/in/noahsmartin/
----
diff --git a/src/content/authors/nx_jameshenry.md b/src/content/authors/nx_jameshenry.md
deleted file mode 100644
index b0cd4822..00000000
--- a/src/content/authors/nx_jameshenry.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: James Henry (Nx)
-avatar: ../../assets/avatars/default.png
-occupation: Director of Engineering @ Nx
-twitter: https://twitter.com/mrjameshenry
-github: https://github.com/JamesHenry
----
diff --git a/src/content/authors/nx_miroslavjonas.md b/src/content/authors/nx_miroslavjonas.md
deleted file mode 100644
index 83dc89b6..00000000
--- a/src/content/authors/nx_miroslavjonas.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Miroslav Jonaš (Nx)
-avatar: ../../assets/avatars/default.png
-occupation: Software Engineer @ Nx
-twitter: https://twitter.com/meeroslav
-github: https://github.com/meeroslav
----
diff --git a/src/content/authors/philniedertscheider.md b/src/content/authors/philniedertscheider.md
deleted file mode 100644
index dfb95340..00000000
--- a/src/content/authors/philniedertscheider.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Phil Niedertscheider
-avatar: ../../assets/avatars/philniedertscheider.jpg
-occupation: Senior Software Engineer
-github: https://github.com/philprime
-url: https://philprime.dev/
----
-
-Phil's focus at Sentry is on Apple platforms, maintaining the Apple SDK, building mobile apps and crafting tools to make developers' lives easier.
-His passion is building software to solve problems and surprise with innovation.
diff --git a/src/content/authors/priscilaoliveira.md b/src/content/authors/priscilaoliveira.md
deleted file mode 100644
index 279401ed..00000000
--- a/src/content/authors/priscilaoliveira.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-name: Priscila Oliveira
-avatar: ../../assets/avatars/priscilaoliveira.png
-occupation: Software Engineer
-twitter: https://twitter.com/priscilawebdev
-url: https://priscilawebdev.github.io/priscilaoliveira/
-linkedin: https://www.linkedin.com/in/priscilawebdev
-github: https://github.com/priscilawebdev
----
-
-Priscila Oliveira is a Software Engineer at Sentry, focused on building and improving product features that empower software development teams to do their best work, all while writing open-source code. She also contributes to the open-source library Verdaccio and helps organize technology meetups in Vienna. In her free time, Priscila enjoys traveling, watching series, and spending time with her family and pets.
diff --git a/src/content/authors/rajjoshi.md b/src/content/authors/rajjoshi.md
deleted file mode 100644
index 34d4472e..00000000
--- a/src/content/authors/rajjoshi.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Raj Joshi
-avatar: ../../assets/avatars/rajjoshi.jpeg
-occupation: Software Engineer
-linkedin: https://linkedin.com/in/rajjoshi-/
-url: https://rajjoshi.me/
-github: https://github.com/iamrajjoshi
----
-
-Raj is Software Engineer working on building alerting and notifications systems at Sentry to notify developers when things break where they are.
diff --git a/src/content/authors/scottcooper.md b/src/content/authors/scottcooper.md
deleted file mode 100644
index a69114e9..00000000
--- a/src/content/authors/scottcooper.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Scott Cooper
-avatar: ../../assets/avatars/scottcooper.png
-occupation: Senior Software Engineer
-twitter: https://twitter.com/scttcper
----
-
-Scott Cooper is a San Francisco web developer who's all about using TypeScript to create remarkable digital experiences. Beyond coding, he indulges in tortas and relishes the Pacifica Beach Taco Bell. With an innovative spirit and a palate for flavors, Scott adds a distinctive touch of creativity to the realm of web development.
diff --git a/src/content/authors/sigridhuemer.md b/src/content/authors/sigridhuemer.md
deleted file mode 100644
index 91f2f551..00000000
--- a/src/content/authors/sigridhuemer.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Sigrid Huemer
-avatar: ../../assets/avatars/sigridhuemer.jpeg
-occupation: Software Engineer
-twitter: https://x.com/bitsbysigrid
-github: https://github.com/s1gr1d
-url: https://sigrid.digital
----
-
-Software Engineer for Sentry’s JavaScript SDKs, fascinated by how things work—from code to human society. Also enjoys train travel, books, and photography.
diff --git a/src/content/authors/simoncropp.md b/src/content/authors/simoncropp.md
deleted file mode 100644
index 58e1a502..00000000
--- a/src/content/authors/simoncropp.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-name: Simon Cropp
-avatar: ../../assets/avatars/default.png
-stackoverflow: https://stackoverflow.com/users/53158/simon
-github: https://github.com/SimonCropp
----
diff --git a/src/content/authors/stefanjandl.md b/src/content/authors/stefanjandl.md
deleted file mode 100644
index 4037a734..00000000
--- a/src/content/authors/stefanjandl.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Stefan Jandl
-avatar: ../../assets/avatars/stefanjandl.png
-occupation: Software Engineer
-twitter: https://twitter.com/bitsandfoxes
-github: https://github.com/bitsandfoxes
----
diff --git a/src/content/authors/steveneubank.md b/src/content/authors/steveneubank.md
deleted file mode 100644
index 11d388e2..00000000
--- a/src/content/authors/steveneubank.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Steven Eubank
-avatar: ../../assets/avatars/default.png
-occupation: Product Manager
-twitter: https://twitter.com/steven_boKnows
-linkedin: https://linkedin.com/in/https://www.linkedin.com/in/steven-eubank-72a2316b//
----
diff --git a/src/content/authors/tedkaemming.md b/src/content/authors/tedkaemming.md
deleted file mode 100644
index c72876f4..00000000
--- a/src/content/authors/tedkaemming.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-name: Ted Kaemming
-avatar: ../../assets/avatars/ted.png
-occupation: Engineer
-twitter: https://twitter.com/tkaemming
----
diff --git a/src/content/authors/trevorelkins.md b/src/content/authors/trevorelkins.md
deleted file mode 100644
index 25615d1e..00000000
--- a/src/content/authors/trevorelkins.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Trevor Elkins
-avatar: ../../assets/avatars/trevorelkins.png
-occupation: Staff Software Engineer
-github: https://github.com/trevor-e
-url: https://www.telkins.com
-twitter: https://x.com/rovert_snikle
----
diff --git a/src/content/authors/yagiznizipli.md b/src/content/authors/yagiznizipli.md
deleted file mode 100644
index 0e51ce0c..00000000
--- a/src/content/authors/yagiznizipli.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Yagiz Nizipli
-avatar: https://avatars.githubusercontent.com/u/1935246?v=4
-occupation: Senior Software Engineer
-twitter: https://twitter.com/yagiznizipli
-github: https://github.com/anonrig
----
-
-Senior Software Engineer, Node.js Technical Steering Committee member
diff --git a/src/content/blog/3m-dollar-dropdown.md b/src/content/blog/3m-dollar-dropdown.md
deleted file mode 100644
index eec47024..00000000
--- a/src/content/blog/3m-dollar-dropdown.md
+++ /dev/null
@@ -1,142 +0,0 @@
----
-title: "A $3,000,000 Dropdown"
-date: "2023-11-15"
-tags: ["building sentry", "multiregion"]
-draft: false
-summary: Almost 2 years ago, Sentry embarked on a project to bring true EU data residency to Sentry's customers. We decided to do it the hard way.
-images: ["../../assets/images/locking-war-story-title.png"]
-postLayout: PostLayout
-authors: ["mikeihbe"]
----
-
-### TLDR; Shameless Plug
-
-Sentry is excited to offer EU Data Residency (from Frankfurt, Germany) to our customers on \***\*all\*\*** plan tiers and at _no extra cost_. [Fill out our form for early access](https://sentry.io/trust/privacy/#data-residency-form) or wait for the GA planned in December.
-
-On to the technical goodies…
-
-# The Project
-
-Almost 2 years ago, Sentry embarked on a project to bring true data residency to our customers. We decided to do it the hard way.
-
-We’ve been fully compliant with GDPR through data processor contracts, but we wanted to side-step the lawyers and enable customers to truly host their data in the EU. Many Sentry users, big and small, have been self-hosting Sentry because we were unable to provide in-jurisdiction data storage for them.
-
-Superficially, supporting the EU is as simple as adding a dropdown to our organization creation flow. We did in fact do that, but there is a mountain of work that happened behind the scenes that we want to share!
-
-This is the $3M dropdown in the Sentry organization creation flow that sets where your customer data is stored:
-
-
-
-> 💰 ~15 people working part or full time over >18 months in San Francisco and Toronto easily tops $3M.
-
-# Optimizing for User Experience
-
-One of the primary goals of this project was to deliver a great user experience for our customers. This drove most of our decision-making.
-
-A simple implementation would've involved deploying a completely disjointed instance of Sentry in the EU. Unfortunately, this would’ve been a terrible user experience for many customers.
-
-You’ve probably experienced the bad UX I’m talking about with other products. Every time you want to log in, you have to tell them your email address or the name of your organization, then they send you a link to the right URL where you can actually log in.
-
-We wanted to avoid that rigamarole – for several reasons. About half of Sentry’s users are in multiple Sentry organizations, and those organizations can now be in different data centers. Imagine having to wait for an email link every time you wanted to switch organizations?
-
-We wanted to do better. We wanted to maintain your ability to seamlessly toggle between Sentry organizations anywhere in the world.
-
-
-
-We also have 1000s of organizations that share the same 3rd party integration target with multiple Sentry organizations. Sometimes several teams within a company will create their own Sentry orgniazations but share a single GitHub account or Slack workspace. Or a parent company can have many subsidiaries (each with their own Sentry organization) that all share a Jira instance. We didn’t want to break any of these customer workflows just because customers opt to have organizations in multiple locales.
-
-We worked hard to design a solution that maintained these optimal experiences, and that was as backward compatibile and as fault-tolerant as possible.
-
-# An architecture that optimizes for UX
-
-## First, some context
-
-Sentry is a monolithic Python [Django](https://www.djangoproject.com/) application deployed in several form factors (web servers, [Kafka](https://kafka.apache.org/) consumers, [Celery](https://github.com/celery/celery) workers, etc). The application is backed by several [PostgreSQL](https://www.postgresql.org/) and [Redis](https://redis.io/) clusters, Google Cloud Storage, [Snuba](https://github.com/getsentry/snuba) and [Clickhouse](https://clickhouse.com/) clusters, and a bunch of other services like [Relay](https://github.com/getsentry/relay) and [Symbolicator](https://github.com/getsentry/symbolicator) that are associated with our processing pipeline.
-
-> 🪚 Our job was to take this monolithic hydra of an application and surgically divide all the pieces that need to be centralized from all the customer data that needs to be localized all while the application is running and 100ish other engineers are working on the project.
-
-Some stats to illustrate the scope of the sentry monolith:
-
-
-
-## Our approach
-
-The most important thing we did was to articulate a clear difference between Sentry user data (its users) and Sentry’s customers’ data (event data). Customer event data must never leave the region it was sent to, but Sentry user data has to be reachable everywhere.
-
-## Splitting the data model
-
-Given those constraints, we began by introducing the concept of “silos”. A single “control silo” contains globally unique data and many “region silos” contain Sentry’s customer’s data.
-
-The control silo holds globally unique information like organization data, user accounts, slugs, and integration configuration. The region silos contain all of an organization’s events, projects, and other customer-specific information.
-
-We then assigned each model to a silo and went about breaking all foreign keys between models located in different silos. This required roughly 80 migrations as well as refactoring all queries that joined data across silo boundaries to either fundamentally change how they work or replace them with RPC calls.
-
-
-
-## Cross silo interaction
-
-Customer data cannot be fetched from region silos, which is the key principle of the design, but there are lots of cases where region silos need user information to check permissions or fetch notification settings to properly send an alert. To handle these cases, we ended up building two primary mechanisms: RPCs and a transactionally written event queue (aka an “outbox”) that we use to ensure eventual consistency.
-
-### Remote Procedure Calls
-
-We ended up building a fairly standard RPC implementation that allows us to define a pure python interface that accepts simple dataclasses as arguments, then we provide a single concrete implementation of the interface. We have some decorators and helper methods that wrap up the interface and autogenerate an HTTP client implementation that handles arguemnt serialization, etc and connects to our RPC endpoint that re-marshals the data and handles dispatching to the concrete implementation.
-
-[—> Go see some code —>](https://github.com/getsentry/sentry/tree/master/src/sentry/services/hybrid_cloud)
-
-#### Why not GRPC?
-
-We strongly considered it, but adopting a code gen tool for this was a bit controversial internally, and this wasn’t that hard to implement, so we just built it 🤷.
-
-### RPC Versioning
-
-Breaking API changes can be a big problem. Particularly when we have to run different versions of Sentry all over the world in a backward compatible way. That's why we are building a tool that publishes the OpenAPI spec for our RPC interfaces and can detect incompatible version drift (like removing arguments or adding arguments without a default). In practice, so far at least, these APIs don’t change much, so we’ve been prioritizing shipping this to customers, but we’ll be circling back to ship this to ensure that all of our cross-silo communication remains stable.
-
-### Cross Region Replication
-
-We had some hearty debate about how to handle data replication where we needed it. We wanted to strike the right balance between network efficiency, explicitness, and ease of correct use. We didn’t want to require intervention from our ops team to replicate a new table, but we also didn’t want it to be too easy for developers to move data around that they shouldn’t be moving. Explicitness was key to the implementation so we could confirm correctness.
-
-#### Why not Change Data Capture (CDC)?
-
-Sentry has a lot of wildly varying deployment modes that we need to handle: self-hosted, development environments, test suites, single tenants, and production SaaS. Our replication needs also occasionally involve business logic that needs to be tested. Between the operational complexity and our need for custom business logic, we opted to fully define our replication implementation within the application logic so that it would just work everywhere and be easy to test.
-
-### Cross silo synchronization
-
-#### Webhook Proxying
-
-Sentry receives webhooks from many third parties, ranging from repository providers like GitHub to payment processors like Stripe. Many of these integrations only allow for a single webhook destination. This created a significant challenge for our multi-region architecture: where should webhooks go? We chose a design that receives all integration webhooks in the Control Silo. Once received, webhook payloads are stored as outbox messages which are delivered to the relevant region as if it came from the integrating service directly. This design allowed us to avoid rewriting complex webhook handling logic and focus our efforts on extracting routing information from webhooks and proxying hooks to the relevant region in an eventually consistent way.
-
-#### 3rd Party Integrations
-
-Sentry allows third party integrations to be shared across organizations. This was no problem when all requests to the third party originated from the same place, but now these organizations can be in different regions. This causes a problem: any of the requests to a third party from any region could trigger an OAuth token refresh that other silos need to be immediately aware of. Eventual consistency driven by our outbox system is insufficient for meeting this requirement.
-
-
-
-In order to provide this synchronization, we built an OAuth aware 3rd party proxy that handles token refreshes and allows us to maintain a single source of truth for OAuth tokens. The trade off for correctness here is that all 3rd party API traffic has to go through this proxy that lives in the control silo.
-
-## Learnings & Challenges
-
-This post was only able to scratch the surface of the many interesting challenges we encountered in this project. We’ll be covering many of these topic in more detail in an upcoming blog series after we go live.
-
-| | |
-| --------------------------------------- | ------------------------------------ |
-| Customer Domains | Deploy Pipelines |
-| Upgrading Columns to BigInt | Audit Logs & User IP Records |
-| Distributed ID Generation | Admin UX Changes |
-| Cross Region Replication | Move Marketo Domain |
-| Control Silo Webhook Forwarding | Update ETL for Control Silo |
-| Endpoint Allocation & Enforcement | Update ETL for Region Silos |
-| Model Allocation & Enforcement | Dangerous Migrations in Multi-Region |
-| API Gateway | Relay in Each Region |
-| Infra Provisioning for Regions | Datadog Observability Consolidation |
-| Infra Provisioning for Control | RPC Implementation |
-| Region Selection UX | CI with Logically Split DBs |
-| New APIs for Non-Organization Resources | CI with Actually Split DBs |
-| Feature Flagging Across Regions | |
-
-If there's anything specific you'd like to hear more about, hit us up on [Discord](https://discord.gg/ez5KZN7) or [Twitter](https://twitter.com/getsentry)!
-
-# How do I get it?
-
-We’re currently in the process of launching the new EU region. If you’re interested in early access, you can sign up here https://sentry.io/trust/privacy/#data-residency-form.
-
-We also have new tooling coming soon to support relocating organizations from self-hosted to EU/US region as well as between the EU/US regions. We’ll update here as soon as it’s available.
diff --git a/src/content/blog/alias-an-approach-to-net-assembly-conflict-resolution.md b/src/content/blog/alias-an-approach-to-net-assembly-conflict-resolution.md
deleted file mode 100644
index 274693cc..00000000
--- a/src/content/blog/alias-an-approach-to-net-assembly-conflict-resolution.md
+++ /dev/null
@@ -1,94 +0,0 @@
----
-title: "Alias: An approach to .NET Assembly Conflict Resolution"
-date: "2022-02-24"
-tags: [".net", "sdk"]
-draft: false
-summary: "Most plugin based models load all assemblies into a single shared context. This is a common approach because it has better memory usage and startup performance. The history and rules of assembly loading in .NET is convoluted; its current status makes it difficult (and sometimes impossible) to load multiple different versions of the same assembly into a shared context. Instead of trying to struggle with existing options we decided to build a new tool: Alias."
-images: []
-postLayout: PostLayout
-canonicalUrl: https://blog.sentry.io/2022/02/24/alias-an-approach-to-net-assembly-conflict-resolution/
-authors: ["brunogarcia", "simoncropp"]
----
-
-Many .NET applications and frameworks support a [plugin based model](). Also known as “add-in” or “extension” model. A plugin model allows extension or customization of functionality by adding assemblies and config files to a directory that is scanned at application startup. For example:
-
-- [MSBuild tasks](https://docs.microsoft.com/en-us/visualstudio/msbuild/task-writing)
-- [Visual Studio extensions](https://docs.microsoft.com/en-us/visualstudio/extensibility/starting-to-develop-visual-studio-extensions)
-- [ReSharper](https://www.jetbrains.com/resharper/)/[Rider](https://www.jetbrains.com/rider/) plugins
-- [Unity Plugins](https://docs.unity3d.com/Manual/Plugins.html)
-
-## The problem
-
-Most plugin based models load all assemblies into a single shared context. This is a common approach because it has better memory usage and startup performance. The history and rules of assembly loading in .NET is convoluted; its current status makes it difficult (and sometimes impossible) to load multiple different versions of the same assembly into a shared context.
-
-For example, it isn’t possible to load both versions 12.0.2 and 12.0.3 of `Newtonsoft.Json.dll` into the same context. In a plugin environment, the resulting behavior is often based on the load order of plugins. At runtime, the reference used in the first loaded plugin is then used by every subsequent plugin. So if a plugin relies on a later version of a reference than the one initially loaded, that plugin will fail either at load time or at runtime. A similar conflict can occur at compile time if the build tooling had conflict detection in place.
-
-More specifically in the Unity world, [UPM (Unity Package Manager)](https://docs.unity3d.com/Manual/upm-ui.html) packages can include one or more DLLs that can cause such conflicts when used together. With Unity adding support for .NET Standard 2.0, different package developers (including Unity themselves) began bundling some `System` DLLs such as `System.Runtime.CompilerServices.dll`, `System.Memory.dll`, and `System.Buffers.dll`.
-
-Since the release of .NET 5.0, many of these DLLs have become part of the standard library—meaning, now there’s no need to bring them in via NuGet or bundle in a UPM package. The Sentry SDK for .NET is dependency-free when targeting .NET 5 or higher, so no conflict would happen if we could use that instead of .NET Standard 2.0. Unity is [skipping .NET 5 but is working towards supporting .NET 6](https://forum.unity.com/threads/unity-future-net-development-status.1092205/). Unfortunately though, it will take years until all Unity LTS versions are running .NET 6, and we required a solution to unblock a growing number of users hitting issues caused by more than one UPM package bundling the same DLLs, often with different versions.
-
-## Options we considered and ruled out
-
-### [Costura](https://github.com/Fody/Costura)
-
-Costura merges dependencies into a target assembly as resources. We add custom assembly loading logic to the target assembly, so that dependencies are loaded from resources instead of from disk.
-
-The important point here is that the assemblies are not changed. Therefore, those assemblies each still have the same assembly name and, when loaded, will respect the standard assembly loading logic. So in a plugin environment, using Costura will still result in a conflict.
-
-### [ILMerge](https://github.com/dotnet/ILMerge) / [ILRepack](https://github.com/gluck/il-repack)
-
-ILMerge and ILRepack work by copying the IL from dependencies into the target assembly. So the resulting assembly has duplicates of all the types from all the dependencies and no longer references those dependencies. This approach does resolve the conflict—however, both these projects are not currently being actively maintained. For example, both have known bugs related to .NET Core and portable PDBs.
-
-## The solution
-
-With the other existing options exhausted, we decided to build a new tool: Alias.
-
-### [Alias](https://github.com/getsentry/dotnet-assembly-alias/)
-
-Alias performs the following steps:
-
-- Given a directory containing the target assembly and its dependencies.
-- Rename all the dependencies with a unique key. The rename applies to both the file name and the assembly name in IL.
-- Patch the corresponding references in the target assembly and dependencies.
-
-The result is a group of files that will not conflict with any assemblies loaded in the plugin context.
-
-One point of interest is that the result is not a single file, which is the approach used by ILRepack, ILMerge, and Costura. This is because the reviewed plugin scenarios all supported a plugin that was deployed to its own directory as a group of files. Because of that, having a ‘single assembly’ was not a problem we needed to solve.
-
-This allowed the Sentry UPM package to include “its own version” of the supporting `System` DLLs needed to work in a .NET Standard 2.0 target. IL2CPP’s linker still takes care of dropping any unused code in the final application.
-
-Given Sentry’s commitment to support Unity’s LTS version from 2019.4 onwards, we expect to rely on this solution for a few years—until the lowest-supported Unity version allows us to include only `Sentry.dll` without any transient dependencies.
-
-## How to use
-
-Alias is shipped as a [dotnet CLI tool](https://docs.microsoft.com/en-us/dotnet/core/tools/). So the [Alias tool](https://nuget.org/packages/Alias/) needs to be installed:
-
-```bash
-dotnet tool install --global Alias
-```
-
-Alias can then be used from the command line:
-
-```bash
-assemblyalias --target-directory "C:/Code/TargetDirectory"
- --suffix _Alias
- --assemblies-to-alias "Newtonsoft.Json.dll;Serilog*"
-```
-
-The `--suffix` should be a value that is unique enough to prevent conflicts. A good candidate is the name of the plugin or some derivative thereof.
-
-You can use Alias to resolve conflicts in your UPM packages too. Like the [Sentry SDK for Unity](https://github.com/getsentry/sentry-unity), our tools are open source.
-
-## Better MSBuild integration
-
-Currently, Alias is only a dotnet tool (command line). You can use it as part of the bundling/packaging step of the development life-cycle. However, it using as part of the bundling/packaging step can make it difficult to debug if something goes wrong, as unit tests and running a project from the IDE don’t automatically use the aliased assembly.
-
-## Package shading draft
-
-NuGet currently has a draft proposal for [Package shading](https://github.com/dotnet/designs/pull/242).
-
-Producer-side package shading is an experimental feature that allows a NuGet package author to “shade” a dependency: embed a renamed copy of it in their package. This ensures that consumers of the package get the exact same version that the package author intended, regardless of any other direct or indirect references to that dependency. This is a feature [available on Maven](https://maven.apache.org/plugins/maven-shade-plugin/).
-
-This is effectively a combination of the techniques used by Alias and Costura. In theory it should solve the same assembly conflict issues that Alias does. Note that this is a draft for a proposed experiment with no current timeline for delivery.
-
-Even though this won’t resolve the problem with Unity UPM packages that we’re using Alias for, it’s great that .NET is considering a longer term solution. [Alexandre Mutel from Unity mentioned](https://twitter.com/xoofx/status/1496898026765438976) a PR in Unity to improve this too.
diff --git a/src/content/blog/better-code-rendering-through-virtualization.md b/src/content/blog/better-code-rendering-through-virtualization.md
deleted file mode 100644
index 32f64950..00000000
--- a/src/content/blog/better-code-rendering-through-virtualization.md
+++ /dev/null
@@ -1,319 +0,0 @@
----
-title: Better Code Rendering Through Virtualization
-date: 2024-12-03
-tags: [javascript, codecov, virtualization, react]
-draft: false
-summary: How we rebuilt Codecov's code renderer from the ground up to be faster and more efficient, utilizing virtualization.
-images:
- [
- ../../assets/images/better-code-rendering-through-virtualization/understanding-root-cause-flamegraph-3.png,
- ]
-postLayout: PostLayout
-canonicalUrl:
-authors: [nicholasdeschenes]
----
-
-**TL;DR: we rebuilt Codecov’s code renderer from the ground up utilizing virtual lists and some other nifty tricks to significantly decrease render blocking time, and unblock customers with files containing tens of thousands of lines.**
-
-## The Problem
-
-
-
-We had Jake an engineer at Microsoft working on TypeScript tooling, reach out to us, letting us know he was devastated with the code renderer crashing on them while trying to render TypeScript’s `checker.ts` file. The problem turns out to be that the current code renderer was not built to handle files that contains this amount of code and coverage data, leading the application to crash. The objective of our initiative is to rebuild our code renderer from the ground up utilizing new techniques so that we’re able to handle these larger files with ease. We will have to figure out how to carry over features of the current code renderer such as native search, scrolling to line, and highlighting line by line coverage.
-
-### Reproducing the Issue
-
-The first step in any debugging scenario was to reproduce the issue that the user was running into. This was fairly simple to do once we found the correct repo and file. You can see below, once we navigate to this file the page starts to _load and load and load_ and finally the tab crashes:
-
-
-
-Clearly there’s something going on here. If the app is unable to render the file, we should show a message to the user rather than the page becoming unresponsive, and ideally we should be able to render any file. So let’s dive a bit deeper into understanding what is going on.
-
-### Understanding the Root Cause
-
-For debugging purposes, we’re going to use a large file that we know won’t crash the browser while rendering so we can understand everything going on. For this example, we’re going to use a large test file from Codecov’s worker test suite, which you can view [here](https://app.codecov.io/github/codecov/worker/blob/main/services%2Fnotification%2Fnotifiers%2Ftests%2Funit%2Ftest_comment.py).
-
-There are two things we are looking for to understand where the performance bottleneck is coming from. Is it our highlighting/tokenization package, or is it React struggling to render a lot of elements? We can utilize the browser dev tools to see what/where/and how things are getting called, we use this information to narrow down where our problem is. Let’s first take a peek at the high level overview of what’s going on while we’re rendering the code renderer:
-
-
-
-We can see it takes it takes around 5 seconds to render the example file, and there’s quite a few things going on here. Let’s try and actually find out where we are tokenizing the content, and then rendering it to the screen:
-
-
-
-We can see from the call stack here that this task here is being called from the tokenizing package `react-prism-renderer`, we can then infer that this is the task we are looking for. We can see that the total time for this task is the work being done to process the file and provide us the tokens for render takes around 80-90ms for this file, which isn’t that bad in the big picture.
-
-Let’s take a peek at all those other function calls:
-
-
-
-Diving a bit deeper into the flame graph we can see that these calls are actually React batch rendering the UI. Looking at this flame graph we can see that the majority of our blocking time is actually related to rendering and React struggling to flush to the screen.
-
-To figure out roughly what our approximate render time per line is, we can use the following equation:
-
-
-
-Entering our values from our example, we get the following equation:
-
-
-
-If we take this value **0.897ms** on average for rendering time, we can extrapolate that out to the example provided by our user, TypeScript’s `checker.ts` file, which has **52283** lines. Taking our rough estimate for rendering time per line and multiplying it with our line count from `checker.ts` we get the following equation:
-
-
-
-With the rendering time taking roughly **46.90s** we can ascertain that the underlying issue for rendering larger files is not the tokenization of those files, but actually the React attempting to render all of the tokenized content. The easiest solution for us to resolve this is to render less content to the screen, to accomplish this we can virtualize the code renderer.
-
-## Requirements
-
-Before rebuilding our code renderer, it’s important to define clear requirements to ensure the solution meets user needs and technical expectations. These requirements will guide development and set measurable goals for success. For this project, they are:
-
-- **Ability to Render Large Files**: The new renderer must efficiently handle files of significant size, such as TypeScript’s `checker.ts`, without crashing or compromising performance.
-- **Preserve Native Search Functionality**: Users should continue to have seamless access to native search capabilities (e.g., `Ctrl/Cmd + F`) to locate content within large files quickly.
-- **Support for Automatic Scrolling to a Given Line**: The renderer must support automatic scrolling to a specific line, enabling direct linking to specific lines of code.
-
-### Ability to Render Large Files
-
-To meet the primary requirement of building a renderer that can handle large files, we need to virtualize our code renderer. With virtualization, we’re able to reduce the overall amount of elements being rendered to the screen, rendering only the elements visible to the window and user, reducing the amount of rendering work React has to do.
-
-The easiest way to introduce virtualization to your application is through using a third-party library, instead of attempting to write the logic ourselves. There are plenty of options out there for virtualization libraries for almost any framework, for our application however, we landed on `@tanstack/react-virtual` as it meets all the functionality requirements for our project, as well we are already using a couple other TanStack libraries and have had pretty good success with them.
-
-For our renderer, we utilized the `useWindowVirtualizer` hook. The reason we choose this specific hook is that unlike the generic `useVirtualizer`, it is designed to set an elements height proportionally to that of the virtual content so the user is actually scrolling via the window rather than inside an HTML element. For our implementation, it looks a little like the following:
-
-```tsx
-import { useWindowVirtualizer } from '@tanstack/react-virtual'
-
-const CodeBody = ({ tokens, /* other props */}) => {
- const virtualizer = useWindowVirtualizer({
- count: tokens.length,
- estimateSize: () => LINE_ROW_HEIGHT,
- overscan: 45,
- scrollMargin: scrollMargin ?? 0,
- })
-
- return (
-
- )
-}
-```
-
-With this we’re able to get our code renderer rendering the content to the screen:
-
-
-
-However, there is a bit more work that we need to do to properly tokenize the content, render the tokens, and style it, which we won’t go into detail here:
-
-
-
-### Preserving Access to Native Search
-
-With the move to virtualization, we are no longer rendering everything to the screen, users are unable to use the built-in browser search to find code they were looking for, as the content is not present. After researching and exploring a few other code renderers on the web, we noticed that they were actually overlaying a one to one mapping of a `textarea` containing the content in its entirety, so that the content was fully present in the DOM as one large string.
-
-After introducing this new textarea we had to to tweak its styles this textarea so that we could overlay the textarea correctly, ensure that it was above the highlighted code for user interactions, ensure that the text styling matched that of highlighted code, and finally make sure that the highlighted code is actually visible. To accomplish this we can use the following styles:
-
-- Setting position to absolute
- - Enabling the `textarea` to overlay the virtual content pixel for pixel
-- Z-index of 1
- - Ensuring that user interactions are handled via the `textarea`
-- Setting white-space to `pre`
- - Preserves newlines and spacing, while ensuring text will not be wrapped
-- Color set to transparent
- - So the user can see the styled code underneath
-
-
-
-#### Resolving Scrolling Challenges with Virtualized Rows
-
-Due to the way virtualization works, and to simplify its implementation, we removed line wrapping for longer lines, aligning with the approach used by other popular code renderers. However, this introduced some new challenges. Since we were no longer rendering the entire highlighted content, the `div` containing the code would dynamically change its width as the user scrolled. Additionally, because the `textarea` overlaid the highlighted content, users were only scrolling the `textarea`, leaving the highlighted code stationary.
-
-The first issue we addressed was the varying widths of each line. We wanted all lines to have the same width to avoid scrolling inconsistencies and allow users to horizontally scroll anywhere. To achieve this, we needed to do two things: track the maximum possible width and apply it to all lines of code. To determine the maximum width, we measured the `textarea`'s width, as it contains all lines of code, and thus the max width possible. We applied this width to the virtually rendered lines, ensuring they are set to the max width possible. You can view the source code for this here:
-
-- Tracking widths: [src/ui/VirtualRenderers/VirtualFileRenderer.tsx](https://github.com/codecov/gazebo/blob/main/src/ui/VirtualRenderers/VirtualFileRenderer.tsx#L253-L254)
-- Applying widths: [src/ui/VirtualRenderers/useSyncTotalWidth.ts](https://github.com/codecov/gazebo/blob/main/src/ui/VirtualRenderers/useSyncTotalWidth.ts)
-
-Now that we have the correct widths set, there’s still an issue where, when the user attempts to scroll, as they’re actually interacting with the `textarea`, causing the highlighted code to remain stationary. To resolve this, we need to synchronize the `scrollLeft` values of the `textarea` and the `div` that contains all the highlighted code and line numbers. This synchronization can be achieved using an event listener that triggers when the user scrolls the `textarea`. Here’s an example of how we accomplish this:
-
-```tsx
-// this effect syncs the scroll position of the text area with the parent div
-useLayoutEffect(() => {
- // if the text area or code display element ref is not available, return
- if (!textAreaRef.current || !codeDisplayOverlayRef.current) return;
- // copy the ref into a variable so we can use it safely in the effect cleanup
- const clonedTextAreaRef = textAreaRef.current;
-
- // sync the scroll position of the text area with the code highlight div
- const onScroll = () => {
- if (!clonedTextAreaRef || !codeDisplayOverlayRef.current) return;
- codeDisplayOverlayRef.current.scrollLeft = clonedTextAreaRef?.scrollLeft;
- };
-
- // add the scroll event listener
- clonedTextAreaRef.addEventListener("scroll", onScroll, { passive: true });
-
- return () => {
- // remove the scroll event listener
- clonedTextAreaRef?.removeEventListener("scroll", onScroll);
- };
-}, []);
-```
-
-### Supporting Automatic Scrolling to Line
-
-Lastly, we need to add support so that users can create shareable links that automatically scroll to a given line when the link is accessed. There are two problems to solve here: adding line numbers for users to interact with and ensuring the page scrolls to the specified line when rendered.
-
-Supporting native searching while adding line numbers introduces additional complexity. Previously, line numbers were rendered alongside the code, but with our virtual rows positioned behind the `textarea`, users cannot interact with them. To address this without adding significant runtime overhead, we need to implement a slightly different approach.
-
-By leveraging our virtualizer and `z-index` properties, we can virtually render the line numbers, keeping them in sync with the virtual code rows. This approach ensures that only a subset of line numbers is rendered at any given time, minimizing the total number of elements.
-
-To make the line numbers interactive, we can use a `z-index` value slightly higher than the `textarea`, allowing the line numbers to overlay it and remain accessible to users.
-
-To create shareable links, we can store the selected line number in the URL's hash property, a common practice among online code renderers. This update to the URL will be triggered when the user clicks on a specific line number.
-
-```tsx
-const CodeBody = ({ tokens, codeContent /* other props */ }) => {
- return (
-
- );
-};
-```
-
-Finally, we need to bring everything together so the application responds correctly when a user navigates to the site. Using a `useEffect` hook allows us to tap into the component's lifecycle, ensuring the effect runs and syncs with the selected line in the URL when the component is rendered.
-
-To prevent the virtualizer from scrolling unnecessarily whenever a user selects a new line, we use refs. Refs are ideal for storing and updating values between renders as they avoid triggering a re-render, and in turn triggering the effect.
-
-For handling the actual scrolling, the `scrollToIndex` method on the virtualizer object provides the functionality we need. By passing the desired index, this method scrolls to the correct position in the viewport and centers the content.
-
-```tsx
-import { useEffect, useRef } from "react";
-import { useWindowVirtualizer } from "@tanstack/react-virtual";
-
-function VirtualCodeRenderer({ rowsOfCode, codeContent }) {
- const initialRender = useRef(true);
- // configure virtualizer
-
- useEffect(() => {
- if (!virtualDivRef || !initialRender.current) return;
- initialRender.current = false;
- const lineNumber = location.hash;
- // little bit of defensive programming ensuring that the
- // line number is within the bounds of the file
- if (lineNumber > 0 && lineNumber < rowsOfCode.length) {
- // because our line numbers are indexed at 1, we need
- // to subtract one to scroll to the right element
- virtualizer.scrollToIndex(index - 1, {});
- }
- }, [location, rowsOfCode.length, virtualizer]);
-
- // render component
-}
-```
-
-## Other Fixes and Improvements
-
-### Using Sentry to Detect Unsupported Languages
-
-During the implementation of the new renderer, we noticed that the amount of languages that we supported highlighting for was not that many, and we should put in some effort to support more languages. However, adding in more language support can have a significant impact on bundle sizes, so we wanted to approach this in a way where we could determine what languages we were missing, and how many use cases there were for the given language. We once again reached for Sentry to capture and create an issue whenever we detected a language that we did not support:
-
-```tsx
-if (supportedLanguage) return supportedLanguage as Language;
-
-Sentry.captureMessage(`Unsupported language type for filename ${fileName}`, {
- fingerprint: ["unsupported-prism-language"],
- tags: {
- "file.extension": fileExtension,
- },
-});
-```
-
-Using the fingerprint, we were able to utilize Sentry’s ability to merge the issues together, even though the message was different for each one. We also created a custom tag so that we could easily see what the frequency of each file extension was so we could determine which languages needed to be added before others:
-
-
-
-We can also see that overtime the amount of events have gone down, validating that our changes have worked, and we’re now highlighting more changes:
-
-
-
-### Disabling Pointer Events for Smoother Scrolling
-
-In our previous renderer we had implemented a small UX improvement where we would disable pointer events while the user was scrolling, and re-enable them after they had finished. This is a fairly common optimization with virtualized libraries as we’re removing calculations the browser has to compute while scrolling while also updating the virtual list and rendering it to the screen. To accomplish this, we have an effect that will add a scroll event listener to the window, and whenever it detects the user is scrolling we update the `pointerEvents` style on our `VirtualCodeRenderer` to `'none'` and queue up an animation timeout to reset `pointerEvents` to `'auto'` 50ms after the user has stopped scrolling.
-
-**Before:**
-
-You can see in the below flame graph, the performance tools have highlighted a couple of events where we are are having to recalculate styles because of forced reflows. When these events occur the browser is forced to calculate layout changes right away instead of batching them.
-
-
-
-**After:**
-
-As you can see in the following trace, we have removed all of these forced reflow events, removing the requirement for the browser to calculate layout changes immediately.
-
-
-
-### Creating Custom Horizontal Scrollbars
-
-Once we had finished up adding virtualization to our main code renderer, we also decided to bring it to our diff renderer, which we use when viewing commits or pull requests on Codecov. It was a fairly straightforward implementation, however we ended up running into an issue with our horizontal scrollbars overlaying the last line of code obscuring it from the user. This happens because our virtualizer is not taking into account the height of the horizontal scroll bar when it estimates the total height of our content.
-
-
-
-We ended up going over to GitHub for some inspiration as to how they handle this issue, and it turns out they actually create their own scroll bar. This scrollbar is placed after the virtual content so it is unaffected from any styling that is being done for the virtual content:
-
-
-
-With this inspiration we decided to create our own scrollbar in the same fashion, this was a fairly trivial implementation utilizing some of the previous hooks we had to create to handle different issues such as syncing scroll positions between all three elements, and ensuring the correct width is being set. We did have to introduce one new hook however, this hook detects whether or not the content is overflowing and requires a custom scrollbar. This hook is hooked up with a `ResizeObserver` so we are able to dynamically add in a scrollbar if the user resizes their window.
-
-
-
-If you’re interested in checking out our implementation you can checkout out the following source code:
-
-- `useIsOverflowing`: [src/ui/VirtualRenderers/useIsOverflowing.ts](https://github.com/codecov/gazebo/blob/main/src/ui/VirtualRenderers/useIsOverflowing.ts)
-- `ScrollBar`: [src/ui/VirtualRenderers/ScrollBar.tsx](https://github.com/codecov/gazebo/blob/main/src/ui/VirtualRenderers/ScrollBar.tsx)
-- `useSyncScrollLeft`: [src/ui/VirtualRenderers/useScrollLeftSync.ts](https://github.com/codecov/gazebo/blob/main/src/ui/VirtualRenderers/useScrollLeftSync.ts)
-
-## The Result
-
-Through virtualizing lists, layering `textarea`'s, and much more, we were able to build a renderer that was easily able to handles TypeScript’s `checker.ts` file. We decided to do some further stress testing to see where the limits would be reached, and we found that to be around 500,000 lines of code. At this point, the browser itself started running into issues running out of memory and crashing.
-
-
-
-At the end of the day, the most important thing we accomplished is being able to provide our users with a great experience (even on their phone):
-
-
diff --git a/src/content/blog/building-a-performant-ios-profiler.md b/src/content/blog/building-a-performant-ios-profiler.md
deleted file mode 100644
index 5977ce78..00000000
--- a/src/content/blog/building-a-performant-ios-profiler.md
+++ /dev/null
@@ -1,100 +0,0 @@
----
-title: "Building a Performant iOS Profiler"
-date: "2022-10-06"
-tags: ["profiling", "mobile", "iOS"]
-draft: false
-summary: Profilers measure the performance of a program at runtime by adding instrumentation to collect information about the frequency and duration of function calls. They are crucial tools for understanding the real-world performance characteristics of code and are often the first step in optimizing a program. In this post, we’ll walk through how we built Sentry’s iOS profiler, which is capable of collecting high quality profiling data from real user devices in production with minimal overhead.
-images: []
-postLayout: PostLayout
-canonicalUrl: https://blog.sentry.io/2022/10/06/building-an-ios-profiler/
-authors: ["indragiekarunaratne"]
----
-
-Profilers measure the performance of a program at runtime by adding instrumentation to collect information about the frequency and duration of function calls. They are crucial tools for understanding the real-world performance characteristics of code and are often the first step in optimizing a program.
-
-
-
-Apple and Google have first party profiling tools, but they are only usable for local debugging during development. Gaining a holistic view of your app’s performance across different devices, network conditions, and other variables requires the collection and aggregation of data from production, and building a profiler that can run under all of these conditions without introducing excessive overhead is a challenging task.
-
-In this post, we’ll walk through how we built Sentry’s iOS profiler, which is capable of collecting high quality profiling data from real user devices in production with minimal overhead.
-
-## Types of profilers
-
-Profilers typically fall into two categories, deterministic and sampling:
-
-- **Deterministic profilers** prioritize accuracy over performance by capturing information about all function calls.
-- **Sampling profilers** collect samples at a fixed interval to limit performance overhead at the cost of only collecting approximate data about function execution that does not have the resolution to determine the duration of all function calls.
-
-
-
-Our goal was to build an iOS profiler that had low enough overhead that it could run in production apps with minimal impact to user experience, which meant that we had to build a sampling profiler. Due to iOS’s sandboxing limitations, the profiler also had to be able to run in-process in the profiled application rather than as an external process.
-
-## How sampling profilers work in-depth
-
-A sampling profiler is a common type of statistical profiler that collects profiling data by periodically collecting samples of the call stacks on each thread and interpolating function durations between samples.
-
-For example, if a call stack capture shows function A calling function B calling function C (A → B → C) and the next capture 10ms later shows A → B without C, we can interpolate the function duration of C to be about 10ms since it existed on the stack at the first sample and no longer exists as of the second sample.
-
-
-
-The _frequency_ of the profiler determines the granularity of the data — for example, a profiler sampling at 100Hz will capture samples every 10ms. Functions that run shorter than 10ms will either not be captured at all if they start and finish execution in between samples, or will be captured but have an inaccurate duration of 10ms if they executed overlapping two samples.
-
-
-
-More frequent sampling allows us to capture shorter running functions, at the cost of more overhead. We chose to sample at a 100Hz frequency because the 10ms resolution is sufficient to find most serious issues without exceeding our overhead target.
-
-There is more than one way to implement a sampling profiler, and the optimal approach largely depends on the environment the profiler is running in. Our first attempt was using an approach that is popular for profiling on \*nix-based operating systems: interrupting threads using a signal handler.
-
-## The first approach: signal handlers
-
-Our first approach was to build a sampling profiler that uses a signal handler to collect the call stack from each thread. This works by having a dedicated sampling thread that fires a signal (`SIGPROF` in this case, a signal specifically intended for profiling) on each thread at the sampling interval, and then collecting the backtrace from inside the signal handler.
-
-
-
-This approach had numerous drawbacks:
-
-- [pthread_kill](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/pthread_kill.2.html), the function we use to fire a signal on a specific thread, intentionally returns an error when firing on worker threads managed by GCD (Grand Central Dispatch). Since most background operations on iOS run on GCD-managed threads, not having this data is a significant drawback. We can work around this limitation by using [syscall](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/syscall.2.html) directly, but this API has been deprecated as of iOS 10.0.
-- Since the call stack had to be captured inside a signal handler, we were limited to using only a small subset of APIs that were considered [“async-signal-safe”](https://man7.org/linux/man-pages/man7/signal-safety.7.html). In addition to making it more difficult to collect the call stack, it also complicated synchronizing access to the data structures that we used to store the collected call stack data from multiple threads.
-- Signal delivery was unreliable. For some threads, we had difficulty collecting any samples, or the gaps between samples were too large. We found [an issue filed in the mono repository](https://github.com/mono/mono/issues/6170) (the open source C# and .NET implementation) that described the same problems we were encountering.
-
-## Second approach: Mach thread suspend
-
-Apple’s Darwin kernel has its own set of APIs for managing threads that is separate from the POSIX thread APIs that are shared across various operating systems. Notably, it has the [thread_suspend](https://developer.apple.com/documentation/kernel/1418833-thread_suspend) and [thread_resume](https://developer.apple.com/documentation/kernel/1418926-thread_resume) APIs, which allow us to suspend a thread to collect its call stack and then resume it afterwards. The design of a profiler built around these APIs looks similar to the signal handler based profiler — we have a sampling thread that periodically grabs a list of threads using [task_threads](https://developer.apple.com/documentation/kernel/1537751-task_threads), suspends each thread, reads its state using [thread_get_state](https://developer.apple.com/documentation/kernel/1418576-thread_get_state) (more on this later), and resumes the thread.
-
-
-
-This approach avoids _most_ of the caveats of the signal handler-based approach. For instance, we can now collect the stacks of GCD-managed threads, and thread suspension works more reliably.
-
-However, we still have to consider some of the same async-signal-safety concerns. For example, if we suspend a thread that currently holds a lock, and attempt to run code that tries to acquire the same lock, the entire process deadlocks. It is often non-trivial to figure out whether a particular piece of code is safe to execute in this scenario — even common operations like allocating memory take a lock.
-
-Independent of our choice of thread suspension method, we reduced the code executing while a thread is suspended to the essentials needed to capture the call stack, using as little indirection as possible and calling only functions that we can reliably assume to never take a lock. All other unsafe work (e.g. thread metadata collection) is done before suspending or after resuming a thread.
-
-## Capturing the call stack
-
-To find out what functions are currently executing on a thread at a given point in time, we capture the list of function pointers by walking the stack. There are two common approaches for walking the stack: by starting with the frame pointer and reading parent frames on the stack by following the linked list of frames, or by reading DWARF (debugging with attributed record formats) debug information encoded in the application binary.
-
-The frame pointer approach is the simplest to implement, but it only works if the the binary is compiled with frame pointer support. The conventions vary by architecture and operating system, but [Apple’s ARM64 ABI](https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms) guarantees the existence of the frame pointer:
-
-The frame pointer register (x29) must always address a valid frame record. Some functions — such as leaf functions or tail calls — may opt not to create an entry in this list. As a result, stack traces are always meaningful, even without debug information.
-
-A similar specification exists for 32-bit ARM on Apple platforms (ARMv6 and ARMv7):
-
-The AAPCS document defines R7 as a general-purpose, nonvolatile register, but iOS uses it as a frame pointer. Failure to use R7 as a frame pointer prevents debugging and performance tools from generating valid backtraces.
-
-Therefore, we can assume that if we are running on iOS, a frame pointer will likely be present (aside from uncommon cases where the frame pointer has explicitly been disabled) and we can use it instead of implementing a more complicated DWARF-based stack walking implementation.
-
-While the thread is suspended, we use thread_get_state to dump the register state of each thread, which allows us to read the frame pointer from its corresponding register (x29 on arm64 or r7 on 32-bit ARM). Each stack frame contains a pointer to the stack frame of the caller, so the stack walking implementation is a loop that starts with the frame pointer and follows the linked list of frames to collect a list of function addresses. This list of addresses that represents the call stack of a given thread is considered a single “sample”.
-
-## Putting together the profile
-
-An iOS profile payload is simply a timestamped series of samples (call stacks), grouped by thread ID:
-
-
-
-The payload is sent by the Sentry SDK to Sentry’s backend where we perform post-processing to symbolicate the function addresses and compute the differences between samples to determine function call durations. We can then use this data to render a visualization called a flamechart that allows a developer to navigate the function call data over the time axis, separated by thread.
-
-## Conclusion
-
-We’ve been testing our iOS profiler with early access customers for the last 5 months and have ingested millions of profiles from real user devices in production. In our benchmarks, the profiler performs with under ~5% average CPU time overhead on a mid-tier iOS device, which satisfies our requirement for a low overhead profiler. The learnings from this project have also helped us start expanding profiling support to additional platforms.
-
-Profiling is available in open beta for all Sentry customers and currently supports native iOS and Android applications. See the documentation to get started!
diff --git a/src/content/blog/building-a-product-tour-in-react.md b/src/content/blog/building-a-product-tour-in-react.md
deleted file mode 100644
index 7e90a864..00000000
--- a/src/content/blog/building-a-product-tour-in-react.md
+++ /dev/null
@@ -1,365 +0,0 @@
----
-title: "Building a Product Tour in React"
-date: "2025-04-11"
-tags: ["react", "typescript", "web", "css"]
-draft: false
-summary: "How we went about building a performant, in-app product tour API using only React"
-images: [../../assets/images/building-a-product-tour-in-react/hero.png]
-postLayout: PostLayout
-authors: [leanderrodrigues]
----
-
-So you made a great app and are ready to start bringing users in. Obviously you feel your design is intuitive, but it’d help to have an onboarding experience to set everyone up for success. We at [Sentry](https://sentry.io/welcome/) found this to be the case while developing a new user interface for our issue details product. To help transition existing power users, newcomers and infrequent visitors, we opted to build out an in-product tour, to provide some pointers on getting around the new look. But, as we’re evolving other parts of the app we wanted to make it generalizable; and I am going to share how we did it.
-
-## Defining Terminology
-
-Before we begin I’m going to be repeating myself quite a bit so lets establish some terminology so we’re all on the same page:
-
-- **Step** - The individual stage of a tour. In practice this constitutes a focused element, and some text description that will usually be an adjacent tooltip
-- **Tour Element** - This is the focused element for a step of the tour. The focusing is done with CSS (as we’ll see), but to appropriately anchor the tooltips, we’ll need to maintain a reference to the actual React component, or DOM element as well.
-
-## Designing a Tour
-
-The main goals of any good product tour are the following:
-
-- Short & Sweet - A user let us interrupt their workflow, let’s respect their time in kind
-- Valuable - We outline the information critical to getting people up to speed, nothing more
-- Focused - The guides draw attention to the exact element we want users to engage with. There’s a whole bunch of ways to do this, but visual distinction is the most important part.
-
-From an engineering perspective though, we wanted a few more things:
-
-- Complete - A tour should not omit steps, nor begin before an element has loaded. To accomplish this, we need to encapsulate the tours, not have disconnected parts.
-- Performance - We want to be able to tour over large, expensive components, as well as tiny buttons, both with the same speed and delightfulness.
-- Flexible - Sure, this is for our project, but let’s build an API that works for future tours
-- Strong Types - TypeScript is very good at throwing red squiggles when you make a typo; let’s use that to our advantage
-
-That last engineering goal is an interesting one. At Sentry, we actually already had [a system for guided tours](https://github.com/getsentry/sentry/blob/25.2.0/static/app/stores/guideStore.tsx), but it had a few limitations. For one, it was disjointed; the [text for each step was separated](https://github.com/getsentry/sentry/blob/25.2.0/static/app/components/assistant/getGuidesContent.tsx) from the [focused element](https://github.com/getsentry/sentry/blob/25.2.0/static/app/components/group/releaseStats.tsx#L116) being put on display, which meant it was challenging to conditionally alter it, use custom styling, or swap in pre-existing components within tour steps. It was also using a storage mechanism that is (_slowly_) on it’s way out from our codebase, so introducing another dependency for a new feature wasn’t ideal. This gives us a **_bonus longer term goal_**, of replacing the legacy system with our new implementation, but I’ll spare you those details to talk about the new and shiny stuff.
-
-## Making it Focused (a.k.a. Awesome)
-
-One of [the core values at Sentry](https://sentry.io/careers/) is that _Pixels Matter_. Even though a product tour is a short, ephemeral moment in the iconic ‘User Journey’, we still want to make it appealing, and unique — that’s just how we do. The design we had arrived at definitely had an impact on the approach I’m going to describe, so maybe it’ll help fill in the gaps for some of the odder choices.
-
-
-
-Let’s break down the styling approach to get to this design:
-
-1. To give the rest of the app a frosted glass look, need to have an element appear above it all when the tour starts. I opted to use visual layering (via `z-index` ) rather than DOM hierarchy since this way, it could be easily omitted or altered for each tour.
-
-```css
-.frosted-glass {
- /* Cover the whole webpage... */
- content: "";
- inset: 0;
- position: absolute;
- /* and float above everything... */
- z-index: 10000;
- /* and prevent mouse interactions... */
- user-select: none;
- /* and make it look neat! */
- backdrop-filter: blur(3px);
-}
-```
-
-2. Then, we’ll want to apply some CSS to a wrapper surrounding the tour element to give it a slick border. By using a higher `z-index` , and pseudo-element (e.g. `::after`) for the border. This will give it the floating, bordered appearance we’re after without layout changes.
-
-```css
-.cool-border {
- /* We're floating the tour element with CSS, not JS! */
- &[aria-expanded='true'] {
- /* Float the element and create a new stacking context... */
- position: relative;
- z-index: 10001;
- /* and ignore user interaction for now. */
- user-select: none;
- pointer-events: none;
- /* Use a pseudo-element to avoid layout shifts... */
- &:after {
- /* and cover the entire tour element... */
- content: '';
- inset: 0;
- position: absolute;
- /* while floating above it... */
- z-index: 1;
- /* with a cool border! */
- border-radius: 6px
- box-shadow: inset 0 0 0 3px #2C2433
- }
- }
-}
-```
-
-3. Next, we need to pop a tooltip above both (yes, a higher `z-index`) with some controls to navigate the rest of the tour and draw focus to it. We currently use [`react-popper`](https://popper.js.org/react-popper/v2/) for our tooltips, but we’re probably due for an upgrade it seems.
-
-We’ll go over this later on, but from the CSS you may notice that our plan is to float the tour element **_with plain old CSS_**! This’ll avoid expensive re-renders in React and help us fulfill our performance goals.
-
-## Building with React Context
-
-Though there may be many options that could have suited the need, we opted to go for [the built-in context provider/consumer APIs](https://react.dev/reference/react/createContext) that ship with React to keep things simple. Since we want this system to be extended and used across Sentry, the API was chosen with that in mind. Here’s the approach I came up with:
-
-### The Tour Provider
-
-My expectations of a dev adding a new tour, are to complete the following steps:
-
-1. Adds an [Enum](https://www.typescriptlang.org/docs/handbook/enums.html) for the unique tour steps.
-2. Specifies an order for these steps with an array
-3. Use `React.createContext` to build a context with these values
-4. Distribute the new context via the generalized provider with full type specificity
-
-On its face, this seems pretty onerous on the developer building out a new tour with our API, but it actually only works out to a few lines of code. Here’s an example of the declaration for the new issue details tour:
-
-```tsx
-import { createContext, useContext } from "react";
-import type { TourContextType } from "sentry/components/tours/tourContext";
-
-export const enum IssueDetailsTour {
- AGGREGATES = "aggregates",
- FILTERS = "filters",
- EVENT_DETAILS = "event-details",
- NAVIGATION = "navigation",
- WORKFLOWS = "workflows",
- SIDEBAR = "sidebar",
-}
-
-export const ORDERED_ISSUE_DETAILS_TOUR = [
- IssueDetailsTour.AGGREGATES,
- IssueDetailsTour.FILTERS,
- IssueDetailsTour.EVENT_DETAILS,
- IssueDetailsTour.NAVIGATION,
- IssueDetailsTour.WORKFLOWS,
- IssueDetailsTour.SIDEBAR,
-];
-
-export const IssueDetailsTourContext = createContext | null>(
- null,
-);
-```
-
-This helps create some rigidity for our types that will avoid bugs as we build out the tours themselves (e.g. not noticing you misspelled `aggraggates`, or forgetting a tour element for `sidebar`), though most of the types have been removed from the snippets for simplicity.
-
-Now, there’s some shared logic that we’ll want across every tour that might be useful to have (e.g., going to the next step, dismissing it, registering new steps) and with separate contexts, we have to be smart about how we share that logic.
-
-The solution for this involves a provider that _doesn’t know what context it’s providing_, instead, we’ll pass that in as a prop and build out our shared logic inside, passing the results through as context. The [initial provider](https://github.com/getsentry/sentry/blob/1c6082133202c1936a43ec89877d03280bd83ada/static/app/components/tours/components.tsx#L21-L78) was bit different, but here’s basically how it works in pseudocode:
-
-```tsx
-export function TourContextProvider(props) {
- // It's a little odd to accept context as a prop, but that's how we pass it
- // along to the element consumers. TourContext here is the result React.createContext(...).
- const {TourContext: React.Context} = props;
-
- // 1. Create some state for managing this specific tour
- // 2. Create some helpful callbacks to navigate (e.g. nextStep(), prevStep())
- // 3. Create a registry for the tour steps
-
- return (
-
- {/*
- It's ALSO a little odd to render actual DOM elements in a provider, but it's
- a nice way to prevent a tour from omitting the blurring.
- */}
-
- {children}
-
- )
-}
-```
-
-There’s quite a lot we’re going to be doing here, but we can leverage the existing [`React.useReducer` hook](https://react.dev/reference/react/useReducer) to make our lives a little easier and combine steps 1 & 2.
-
-```tsx
-// When working with complex reducers, it can be helpful to pull it out of the hook
-// into its own function. It'll help identify any impure side-effects.
-// See: https://react.dev/learn/extracting-state-logic-into-a-reducer#writing-reducers-well
-function tourReducer(state, action) {
- switch (action.type) {
- case "START_TOUR": {
- // Prevent starting the tour until we've fully registered!
- if (!state.isRegistered) {
- return state;
- }
- return { ...state, currentStepId: action.stepId };
- }
- // ...all the other action declarations (e.g. NEXT_STEP, PREV_STEP, SET_REGISTRATION)
- }
-}
-
-export function TourContextProvider(props) {
- const { TourContext } = props;
- const [state, dispatch] = useReducer(tourReducer, {});
-
- // 3. Create a registry for the tour steps
-
- return (
-
-
- {children}
-
- );
-}
-```
-
-Next, we need a step registry. This registry will allow individual elements to indicate to the tour provider that they are mounted and ready for focusing. By allowing the step elements to do this themselves we can handle complicated scenarios, like pausing access to the tour, while a graph is recalculating, or holding off on starting a tour until after an API call resolves.
-
-Initially I gravitated toward [`React.useState`](https://react.dev/reference/react/useState) for this, but [@Malachi Willey](https://github.com/malwilley) pointed out that we don’t want these steps to cause re-renders of one another as they update the registry, especially with our performance goals. It’s expected these tour steps wrap large (and expensive) portions of the application, so we can swap the state for [`React.useRef`](https://react.dev/reference/react/useRef), and only update state when all of the steps are registered.
-
-```tsx
-type TourRegistry = Set;
-
-export function TourContextProvider(props) {
- const { TourContext, orderedStepIds } = props;
- const [state, dispatch] = useReducer(tourReducer, {});
-
- const [isRegistered, setIsRegistered] = useState(false);
- const registry = useRef(new Set());
-
- // We can add a new helper method to register new step elements
- const handleRegistration = useCallback(
- (stepId: string) => {
- registry.current.add(stepId);
- const isCompletelyRegistered = orderedStepIds.every((stepId) => registry.current.has(stepId));
-
- // Only update provider state when all elements are registered
- if (isCompletelyRegistered) {
- setIsRegistered(true);
- }
-
- // and we can return a cleanup function if the step is unmounted
- return () => {
- registry.current.remove(stepId);
- setIsRegistered(false);
- };
- },
- [orderedStepIds],
- );
-
- return (
-
-
- {children}
-
- );
-}
-```
-
-And with that, we have a provider which we can implement somewhere that wraps all our tour elements. TypeScript will also narrow the types for you, ensuring the props you pass in make sense with your tour.
-
-```tsx
-
- orderedStepIds={ORDERED_ISSUE_DETAILS_TOUR}
- TourContext={IssueDetailsTourContext}
->
- {/* The rest of the page and step elements go here */}
-
-```
-
-### The Tour Consumer
-
-The context provider from the previous stage had a few steps to get it set up, but it’s a one and done affair. The tour element however, needs to be implemented for every step of the tour you’re building out, meaning we need to keep it as simple as possible. Here are the essentials this component needs to know:
-
-1. Which context it’ll be using
-2. Which step it is responsible for
-3. What content to display when it’s active
-
-Just like before, the [original attempt is a bit complex](https://github.com/getsentry/sentry/blob/1c6082133202c1936a43ec89877d03280bd83ada/static/app/components/tours/components.tsx#L108-L201), so let’s look at a basic implementation:
-
-```tsx
-export function TourElement({ children, TourContext, ...props }) {
- // Check the context from props
- const tourContextValue = useContext(TourContext);
- // If we don't find anything, fallback to the children
- if (!tourContextValue) {
- return children;
- }
- // Otherwise, render the custom component wrapping the children
- return (
- {...props} tourContextValue={tourContextValue}>
- {children}
-
- );
-}
-
-export function TourElementContent({ children, tourContextValue, title, description, stepId }) {
- // Add this step to the register
- const { handleStepRegistration } = tourContextValue;
- useEffect(() => handleStepRegistration(stepId), [stepId, handleStepRegistration]);
-
- // Manage the tour from the passed in context
- const { dispatch, state } = tourContextValue;
- const isActive = state.currentStepId === stepId;
- return (
-
-
- {children}
-
- {isActive && (
-
-
{title}
-
{description}
-
-
-
- )}
-
- );
-}
-```
-
-The only reason we have two components here to make our lives easier in assuring that `tourContextValue` exists for `TourElementContent`, even if it might not for `TourElement`. This will also enable us to use hooks with that data. The `.cool-border` component will raise the `z-index` of our children (the tour element) so it _floats_ above the `.frosted-glass`.
-
-I want to highlight a consideration we’ve made with how we’ve chosen to render the children and tour content in `TourElementContent` . **_We are not re-parenting `children` ever!_** This is critical to avoiding layout shift and keeping your app performant. It may be tempting to do something like this:
-
-```tsx
-if (isActive) {
- return children;
-} else {
- return
{children}
;
-}
-```
-
-But doing so will mount/unmount the children, which runs the risk of expensive re-renders, layout recalculations, api calls or whatever other side-effects your components may produce! Instead, we’ll just restyle the wrapper using state allowing us to wrap anything from an entire page, to a small button, without impacting tour element or page around it.
-
-To use what we’ve built, it’s as easy as wrapping the focused element:
-
-```diff
-+
-+ tourContext={IssueDetailsTourContext}
-+ stepId={IssueDetailsTour.AGGREGATES}
-+ title="Check out the new graph"
-+ description="Add filters, pick a date range, and watch it change"
-+>
-
-+
-```
-
-And now, we can start the tour however we want from within our provider by dispatching the appropriate action. We can have this trigger with any arbitrary conditions we want (e.g., users created after date X, organizations of subscription plan Y), and the the tour providers/elements don’t need know about it. Concerns are separated 👍
-
-```tsx
-const { dispatch, isRegistered } = useContext(IssueDetailsTourContext);
-return (
-
-);
-```
-
-
-
-## So, it’s done?
-
-Nope. I was the first ‘user’ of the new tour API for the issue details page, so I missed some of the usability pitfalls. I had put together the basics; some tests, a [storybook](https://sentry.io/orgredirect/organizations/:orgslug/stories/?name=app%2Fcomponents%2Ftours%2Ftour.stories.tsx) page and doc strings where they were relevant, but an outside perspective helps quite a bit. Soon after the first tour launched, we wanted to build another for some new navigation updates, and [Malachi](https://github.com/malwilley) found a few quality of life improvements:
-
-- [[#87810](https://github.com/getsentry/sentry/pull/87810)] Often times, `dispatch(...)` calls would need to be followed by some callback (perhaps tracking analytics, making an API request, etc.) but side-effects are not permitted in a reducer function. Instead, we can allow the hook to couple the action dispatch + callback, and we could use the function without repeating ourselves all over!
-- [[#87805](https://github.com/getsentry/sentry/pull/87805)] I had coupled some state for whether or not a tour was actually available to the current user that ended up complicating things. The tour itself doesn’t need to keep track of it’s availability, since that’s highly dependent on the tour. Instead, let it worry about managing the steps, and navigation, while we control access to the tour from outside these components.
-
-## Can it be better?
-
-Probably, but at the same time, ‘better’ is a moving target. For our purposes, it works great! We’ve got some excellent feedback about it and didn’t need to overcomplicate things with a third-party library, so it’s a success. Long term, there might be a few areas we can improve upon:
-
-- Currently, one tour = one context provider, so if we create many more tours, it might get a little messy. A helpful refactor could allow us to create a catch-all provider that manages the internal state of all tours, something like a `TourStore` . We could also use this to prevent concurrent tours.
-- The wrapper `.cool-border` element will always render in-place of the tour element. Depending on the tour element, this could produce some invalid DOM structure, for example nesting a `div` inside a `ul`. To get around this, we can pass in a custom wrapper as a prop to use instead, or forward the styles directly to the children to get rid of the extra element.
-
-## Wrapping Up
-
-Thanks to [the many reviewers](https://github.com/getsentry/sentry/pull/85900) who helped shape the API, and another callout for [Malachi](https://github.com/malwilley) who sanded down some of the rougher edges.
-The current (April 9, 2025) version of the tour provider is available to inspect [on GitHub](https://github.com/getsentry/sentry/tree/054082d90608639d513838ec6bd17985fd06e4cd/static/app/components/tours), and you may have already taken a few of these tours in Sentry already (and if you have some thoughts please let us know!). We added some nice-to-haves, like navigation via keyboard, focus scoping and new styles, but the bones still match what we’ve gone over today.
-
-The important part, is that it’s live and being used as you read this (probably) and teaching users something new (hopefully).
diff --git a/src/content/blog/building-sentry-source-maps-and-their-problems.md b/src/content/blog/building-sentry-source-maps-and-their-problems.md
deleted file mode 100644
index ec24d00b..00000000
--- a/src/content/blog/building-sentry-source-maps-and-their-problems.md
+++ /dev/null
@@ -1,158 +0,0 @@
----
-title: "Building Sentry: Source maps and their problems"
-date: "2019-07-16"
-tags: ["source maps", "debugging", "building sentry"]
-draft: false
-summary: "Other than Python, JavaScript is the oldest platform that Sentry properly supports, which makes sense considering many Python services (including Sentry itself) have a JavaScript front-end. The system that almost everybody uses to debug transpiled code (and the hopefully apparent subject of this blog post) is source maps. Today, we want to focus on some of the their shortcomings and why source maps cause problems for platforms like Sentry."
-images: [../../assets/images/building-sentry-source-maps-and-their-problems/sourcemaps.gif]
-postLayout: PostLayout
-canonicalUrl: https://blog.sentry.io/2019/07/16/building-sentry-source-maps-and-their-problems/
-authors: ["arminronacher"]
----
-
-_Welcome to our [series of blog posts](/tags/building-sentry) about all the nitty-gritty details that go into building a great debug experience at scale. Today, we’re looking at the shortcomings of source maps._
-
-Other than Python, JavaScript is the oldest platform that Sentry properly supports, which makes sense considering many Python services (including Sentry itself) have a JavaScript front-end. As the popularity of transpiling grew, the need for tools to debug transpiled code in production became obvious. The system that almost everybody uses to debug transpiled code (and the hopefully apparent subject of this blog post) is [source maps](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.djovrt4kdvga).
-
-A lot could be said about source maps — like how well they fit into Maroon Five lyrics or how they simplify [JavaScript](https://blog.sentry.io/2015/10/29/debuggable-javascript-with-source-maps) and [Node.js](https://blog.sentry.io/2019/02/20/debug-node-source-maps) debugging. Today, we want to focus on some of the their shortcomings and why source maps cause problems for platforms like Sentry.
-
-## Map thy token
-
-Source maps were created to map a token (for instance, a function name) in a minified JavaScript file to a non-minified file. They have enough information to tell us, for instance, a function call in line `1` and column `4021` of a minified file was originally in line `138` and column `8`. Source maps also tell us the original file name and what the original token was called; the token currently called a might have been called `performAction` in the original file.
-
-While helpful, the above information does not fully equip us to understand and debug errors. If you use Sentry with source maps, you should notice that we want you to upload the source maps and the minified JavaScript files. That’s because we look at the minified JavaScript source to perform some basic heuristics.
-
-Minified JavaScript stack traces (to which Sentry often refers) contain information that provides insights into errors. For each frame in the stack trace, we get the name of the function for this frame along with the filename, line number, and column number of where exactly the interpreter was when the stack trace was generated.
-
-Let’s take a non-minified example to understand what points where:
-
-```js
-function myFunction() {
- console.log("stack: " + new Error().stack);
-}
-function outerFunction() {
- myFunction();
-}
-outerFunction();
-```
-
-This little script produces the following stack trace in the console:
-
-```shell
-script.js:2 stack: Error
- at myFunction (file:///tmp/script.js:2:28)
- at outerFunction (file:///tmp/script.js:5:3)
- at file:///tmp/script.js:7:1
-```
-
-The exact output is different depending on the browser, but we generally get the same information. Here is the output for the first frame in the stack trace:
-
-- `myFunction` is the name of the function. In a minified file, this is the minified function name.
-- `file:///tmp/script.js` is the name of the file containing the function. If this is from a minified file, this is the name of the minified file. Note that multiple source files can merge into the same minified file.
-- `2` is the line number (1 indexed). Note that this is not the line where the function was declared but where in the function we were when the stack trace was created.
-- `28` is the column in the function where we were. In non-minified files, this is typically not very important, but, since minified files are generally one long line, this information plays a crucial role in pinpointing where we are within the file.
-
-_One note here: column uses an odd unit of measurement. It’s the offset from the beginning of the line counted in UTF-16 characters. So an emoji offsets the column by `2`, but a character on the basic plane (like `a`, `ä` or `и`) would only offset it by `1`._
-
-Here is the question that Sentry needs to be able to answer:_ if we are in that frame, but the frame is minified, how can we know what the function was originally called_? Notice that we do not know where the function was declared, so we cannot figure out where in the source map we would need to ask for the original function name.
-
-To make this clearer, let’s imagine our script from above would have been minified as such:
-
-```js
-function f() {
- console.log("stack: " + new Error().stack);
-}
-function g() {
- f();
-}
-g();
-```
-
-And this would be the minified stack:
-
-```shell
-stack: Error
- at f (file:///tmp/script.min.js:1:37)
- at g (file:///tmp/script.min.js:1:69)
- at file:///tmp/script.min.js:1:74
-```
-
-So what does `1:37` mean? It points right to the `new Error` in the minified source. If we were to look up the token in the source map at (`0, 36`) (0 indexed), we would be able to find out that the token `new` was originally in file `script.js` on line `2` and column `28`. However, we can’t find out what `f` was called initially because, to get that information, we would need to understand what function declared `new Error`. This information is unfortunately not available in source maps.
-
-Scope information, however, provides the details we need. We need to tell which line to which line this token is valid and what it’s parent scope is. Then, recover the original function name by going back up until we find the function scope that contains all of this.
-
-So what does Sentry to do recover function names?
-
-## Walking over the minified source
-
-Sentry tries to guess function names primarily by tokenizing backward over a minified JavaScript file token by token. In the above example, we start at line 1, column 37 and then go backward token by token until we find a token named `function`. If the token on the right shares the name of the minified token we’re looking for (in this case `f`), we take the location information for `f` (in this case `line 1`, `column 9`) and look it up in the source file. Here, we find that the location is also line `1` and column `9` but in the file `script.js` instead of `script.min.js`, and it was called `myFunction` instead of `f`.
-
-Now, as you can imagine, assuming that the function name f is not reused locally is a flawed approach, as this isn’t guaranteed. One could imagine that a local utility function declared within a function is given the same name as the function outside:
-
-```js
-function f() {
- function f() {
- console.log("x");
- }
- f();
- f();
-}
-```
-
-In this case, we would recover the wrong function name. The anonymous functions and ES6 method syntax are even bigger problems with no viable solutions at the moment.
-
-For example, this looks like an anonymous function using ES6 syntax, but the Browser will give this method the name `foo`:
-
-```js
-let foo = () => {
- throw new Error();
-};
-```
-
-If the demangler goes in and shortens `foo` to `f`, we lose the original function name as we do in other cases. However, as we walk backward token by token from `new Error`, we can’t find the function with basic token scanning. We would have to parse the entire source tree and not just tokenize backward, which is much more work.
-
-## What else could we do?
-
-Well, our ability to recover function names could be better. For instance, function names are not considered for grouping in JavaScript at the moment. In typical situations, Sentry uses the function name as a reliable indicator for grouping, but, in JavaScript, we completely disregard this information because of the unreliability of function names. Different releases of the code might produce differently minified function names, and, when we fail to recover the original one, we produce new groups.
-
-Firefox’s developer tools provide an interesting solution to this problem. Firefox will (optionally) parse both the minified and non-minified files with Babel and try to diff the resulting parse tree to determine scopes.
-
-The problem is hardly new — there are other platforms supported by Sentry that have debug information better than source maps. We support [DWARF](http://dwarfstd.org/), which solves many of these problems and could theoretically be augmented to support JavaScript or WASM. The hacks piled onto this current specification are quite involved, and, honestly, we welcome an improved debug standard.
-
-Just recently, Facebook’s [Metro bundler](https://github.com/facebook/metro) has extended the source map specification for their RAM bundle distribution format and added new Facebook and Metro extensions. Sadly, there is no standardized body that would work on improving the source map standard. After all, the specification itself is still merely a Google doc from many years ago that is floating around the internet.
-
-## Technical implementation
-
-From the technical side, source maps pose a few challenges that are not particularly complex. These challenges group into three points:
-
-1. Safely fetching externally referenced source maps
-2. Processing source maps in a way that is quick and is memory-efficient
-3. Providing support for customer supplied source maps
-
-### Safe fetching
-
-When Sentry receives a stack trace, it needs to find the minified source files to find out which source maps belong to it. Typically, the source map reference is a comment at the bottom of the minified file, but it can also be supplied as a header. In either case, we need to make an HTTP request to get that file. Alternatively, customers can also upload these minified files to us — in which case, we do not need to fetch.
-
-Fetching these files is not particularly complicated, but there are a few critical steps to consider:
-
-- These are an untrusted source, which means we need to ensure that our HTTP client prevents these sources from doing nefarious things.
-- Servers might be really slow to respond and cause load issues. Additionally, servers might try to issue bad HTTP redirects to internal resources we need to prevent.
-- We need to make sure that we do not cause a strain on the customer’s infrastructure where source maps and JS files are typically located. It would not be great if an error reporting tool, upon getting thousands of error report, makes thousands of HTTP requests to get the source maps. We have multiple levels of caching in place to prevent this from happening.
-
-### Processing source maps
-
-We have written about this before (like [here](https://blog.sentry.io/2015/10/29/debuggable-javascript-with-source-maps) and [here](https://blog.sentry.io/2019/02/20/debug-node-source-maps)), and nothing much has changed about how we handle source map processing. We wrote a [source map library in Rust](https://blog.sentry.io/2016/10/19/fixing-python-performance-with-rust), which is optimized to quickly operate and resolve source maps once fetched.
-
-### Customer support source maps
-
-The last part is more of a user interface issue than a technical challenge. Source maps are easy to get wrong. Matching filenames to URLs is complicated for users, and source maps can be insufficient for processing on our side. Our preference is for customers to upload the source maps to us because it lets us do some preprocessing to ensure they work.
-
-Currently, our approach is to let customers use the `sentry-cli` tool we provide, which parses and rewrites source maps before upload and establishes the correct references between the minified file and source map. We use the same library as we do on the server.
-
-Once satisfied, we upload a zip archive with all files to the server. There, our file storage system picks up and stores the files until we need them.
-
-## Improving the ecosystem
-
-Unfortunately, the source map specification has not evolved much over the last few years. In particular, with new technologies like WASM and others, something must change for debugging tools to be able to provide a good experience.
-
-We hope that change will be future collaboration between authors of minifiers and browser/debugger vendors that evolves the source map format to better support scopes, WASM, or non-JavaScript languages, like TypeScript and ReasonML.
diff --git a/src/content/blog/building-sentry-symbolicator.md b/src/content/blog/building-sentry-symbolicator.md
deleted file mode 100644
index ecc3c75d..00000000
--- a/src/content/blog/building-sentry-symbolicator.md
+++ /dev/null
@@ -1,144 +0,0 @@
----
-title: "Building Sentry: Symbolicator"
-date: "2019-06-13"
-tags: ["symbolicator", "building sentry", "native"]
-draft: false
-summary: "Over two years ago, Sentry started supporting its first native platform: iOS. Since then, we’ve added support for many other platforms via minidumps and recently introduced our own SDK for native applications to make capturing all that precious information more accessible. Now, the time has come to lift the curtain and show you how we handle native crashes in Sentry. Join us on a multi-year journey from our first baby-steps at native crash analysis to Symbolicator, the reusable open-source service that we’ve built to make native crash reporting easier than ever."
-images: [../../assets/images/building-sentry-symbolicator/symbolicator.gif]
-postLayout: PostLayout
-canonicalUrl: https://blog.sentry.io/2019/06/13/building-a-sentry-symbolicator/
-authors: ["janmichaelauer"]
----
-
-_Welcome to our [series of blog posts](/tags/building-sentry) about all the nitty-gritty details that go into building a great debug experience at scale. Today, we’re looking at [Symbolicator](https://github.com/getsentry/symbolicator), the service that processes all native crash reports and minidumps at Sentry._
-
-At Sentry, we live to provide the best user experience possible. Over the years, this has led us to optimize Sentry for various platforms and frameworks, such as .NET, Java, Python, JavaScript, and React Native. In addition to the SDKs you use every day to collect events and context from your apps, we also built a fair amount of server-side processing to provide you with high-quality reports.
-
-Over two years ago, Sentry started supporting its first native platform: iOS. Since then, we’ve added support for many other platforms via minidumps and recently introduced our own [SDK for native applications](https://github.com/getsentry/sentrypad) to make capturing all that precious information more accessible.
-
-Now, the time has come to lift the curtain and show you how we handle native crashes in Sentry. Join us on a multi-year journey from our first baby-steps at native crash analysis to Symbolicator, the reusable open-source service that we’ve built to make native crash reporting easier than ever.
-
-## Native code is different
-
-Building a solution for native apps has been a particularly interesting task for us. Due to the lack of a runtime, extracting stack traces or other useful context information is much harder. Without a safety net, native applications can easily crash to a point beyond recovery, with little possibility of sending a crash report to Sentry. Build systems are also vastly different from other ecosystems, as there are many more choices and configuration options, making it harder to provide a streamlined solution.
-
-The common ground for all native applications is that they compile to machine code. At this level, variable and type names are typically gone. Function names only exist in a mangled form, or even optimized away completely. Call stacks of every running thread exist as binary memory regions, with a format defined by the ABI of the CPU or even left up to the compiler implementation.
-
-When native applications crash, for example, due to invalid memory access or illegal instruction, the operating system kernel stops their execution. Depending on the system, a signal is sent to the application, which can be used to react to the crash and run crucial operations before terminating completely. However, since signal handlers aren’t allowed to run any unsafe code, their capabilities are quite limited.
-
-Debuggers — and also Sentry — are presented with the challenge of reading a stack trace from the threads’ call stack memory region, symbolicating them into human-readable function names, and, finally, enriching them with additional information.
-
-
-
-## Stacking your ~cards~ frames
-
-In our initial implementation for iOS, we relied on an open-source framework called [KSCrash](https://github.com/kstenerud/KSCrash) to create a signal handler that catches crashes and computes stack traces. Since iOS is particularly restrictive, we dumped this information into a temporary location and let the application terminate. On the next application launch, our iOS SDK created an event and sent it off to Sentry.
-
-To understand how KSCrash works, we have to take a look at stack memory: the call stack is a continuous list of variable-length frame records. Such a record is pushed when a subroutine is invoked and contains its parameters, local variables, and temporaries. More importantly, however, it includes a special address — the so-called _return address_, which is an instruction pointer telling the CPU where to continue execution once the subroutine completes.
-
-
-
-At any given time, the CPU keeps track of two special pointers in its registers: the current code instruction that is being executed, and the top of the stack. By looking at the top stack frame, one can obtain the return address, i.e., the instruction pointer of the parent frame once it returns. Repeat that for all frames, and you’re left with your stack trace, right? Well… almost.
-
-What we actually have is just the list of return addresses. As a developer, however, you’re interested in where your function was called, and, luckily, there are heuristics to get the actual caller address from the return address. The simplest would be to simply subtract one instruction, as in many cases the call and the return point are subsequent.
-
-
-
-## Walking with dinosaurs
-
-Life is not that simple. In order to walk through frame records in the call stack memory, you need to know the size of each individual frame. To make this easier, compilers emit a frame pointer into every frame pointing to the parent frame. Since the frame pointer is not needed during actual execution, it is usually omitted in release builds to save a few bytes.
-
-Fortunately, debuggers aren’t the only tools interested in being able to walk up the stack. Welcome to the stage: exception handlers. That’s right; every time you throw an exception, some built-in routine needs to unwind the call stack until it hits a frame with an exception handler.
-
-For this purpose, compilers are emitting so-called unwind information, or call frame information. Unwind information indicates the size and contents of all function frame records so that the application or a debugger can walk the stack and extract values like the return address. The information is stored in a condensed binary form in a separate section of the executable so that it can quickly be processed. Convenient, right? Well… almost.
-
-There are still vast differences between various operating systems and CPU architectures. The effective strategy for stackwalking depends on a combination of both and requires reading unwind information in various formats. Unfortunately, the information emitted by compilers is not always accurate or complete due to specific optimizations. Also, certain programs manually manipulate the stack, which can lead to entirely different effects.
-
-At Sentry, we’ve incorporated unwind information handling into our [symbolic](https://github.com/getsentry/symbolic) library. It is built on top of amazing open-source Rust libraries [goblin](https://github.com/m4b/goblin) and [pdb](https://github.com/willglynn/pdb), which provide the lower-level parsing of the different file formats and binary representation. Over time, we also had the pleasure to contribute to those libraries and fix certain edge cases or implement recent additions to the file standards.
-
-For platforms other than iOS, we use [Google’s Breakpad](https://chromium.googlesource.com/breakpad/breakpad/) library to generate minidump crash reports and then process them on our servers. This library contains stack walkers for the most prevalent CPU architectures, which we feed with the unwind information they require to do their job.
-
-## (Debug) information is gold
-
-So far on our journey to native crash analysis, we have obtained a list of instruction pointer addresses. That’s barely a stack trace you could use to debug your applications. You need function names and line numbers.
-
-The final executable no longer needs to know the names of variables or the files that your code was declared in. Sometimes, not even function names play a role anymore. To ensure that developers can still inspect their applications, compilers, therefore, output _debug information_ containing data to connect the optimized instructions with their source code.
-
-However, this debug information can get large. It’s not uncommon to encounter debug information 10 times the size of the executable. For this reason, debug information is often moved (or _stripped_) to separate companion files. They are commonly referred to as _Debug Information Files_, or D*ebug Symbols*. On Windows, they carry a `.pdb` extension, on macOS, they are `.dSYM` folder structures, and, on Linux, there is a convention to put them in `.debug` files.
-
-
-
-The internal format of these files also varies — while macOS and Linux generally use the open-source [DWARF](http://dwarfstd.org/) standard, Microsoft implemented their proprietary CodeView that was eventually [open-sourced](https://github.com/Microsoft/microsoft-pdb) at the request of the LLVM project.
-
-At the heart of each debug information file are tree-like structures explaining the contents of every compilation unit. They contain all types, functions, parameters as well as variables, scopes, and more. Additionally, there are mappings of these structures to instruction pointer addresses in the source code as well as the file and line number where they are declared.
-
-This is precisely the information needed to turn instruction addresses into human-readable stack traces. And, this is essentially what debuggers do; they look up the respective instruction address in the debug file and then display all sorts of stored information. For example, inline functions that no longer have their own stack record as their code has been moved into another function.
-
-
-
-Of course, there are great Rust libraries that can handle debug information, including [gimli](https://github.com/gimli-rs/gimli) for DWARF and `pdb` for CodeView, that are contributing to our improvements. In our own `symbolic` library, we’ve created a [handy abstraction](https://docs.rs/symbolic-debuginfo/6.1.3/symbolic_debuginfo/index.html) over the files and debug formats to simplify native symbolication.
-
-## Speeding it up
-
-Dealing with large debug files also has its drawbacks. At Sentry’s scale, we’ve repeatedly run into cases where we were unsatisfied with the various aspects of retrieving, storing, and processing multiple gigabytes worth of debug information just for a single crash. Additionally, handling different file types all the time only complicates the overall symbolication process — even when hidden behind a fancy abstraction.
-
-Engineers at Google faced the same issue when they created the Breakpad library. They came up with a human-readable and cross-platform representation for the absolutely necessary subset of debug information: Breakpad symbols. And it worked; those files are much smaller than the original files and can easily be handled by engineers.
-
-However, their format is optimized for human readability, not automated processing. Also, certain debug information can’t be stored, such as inline function data, which is a core part of our product. So we decided to create our own format. The objectives: make it as small as possible and as fast as possible to read. And since it needed a name, we pragmatically dubbed it [SymCache](https://docs.rs/symbolic-symcache).
-
-Usually, symcaches weigh an order of magnitude less than original debug files and come with a format that’s easily binary searchable by instruction address. Paired with memory mapping, this makes them the ideal format for quick symbolication. Whenever a native crash comes in, we quickly convert the original debug file into a SymCache and then use that for repeated symbolication.
-
-## Getting the right files
-
-Ultimately, it is all about debug information. Debug information allows Sentry to extract stack traces from minidumps and symbolicate them into useful function names and more.
-
-Because debug information is so vital, minidumps and iOS crash reports contain a list of all so-called images or modules that have been loaded by the process. Most importantly, this list includes the executable itself, but also dynamic libraries and parts of the operating system. Depending on the type, each module contains identifiers that can be used to locate them:
-
-- **Linux (ELF)**: On Linux, recent compilers emit a GNU build id, which is a variable-length hash. There are multiple strategies to compute this hash, from computing a checksum over the code to generating a random identifier. The build id is stored in a program header as well as a section of the file and usually retained when stripping debug information.
-- **macOS (MachO)**: The macOS executable format specifies a UUID in its header that uniquely identifies each build. The dSYM debug companion file matches the same UUID.
-- **Windows (PE, PDB)**: Microsoft’s PDBs specify a GUID and an additional age counter, that is incremented every time the file is processed or modified. Together with the file name of the PDB, they form the debug identifier triple. It is written into the PE header so that it can be located by just looking at the executable.
-
-Sentry displays these identifiers on the Issue Details page and in the metadata of all debug information files. You can also use `sentry-cli` to inspect the identifier and other useful high-level information of your debug files locally:
-
-```shell
-$ sentry-cli difutil check MyApp.dSYM/Contents/Resources/DWARF/MyApp
-
-Debug Info File Check
- Type: dsym debug companion
-
- Contained debug identifiers:
- > 8fbdb750-4ea9-3950-a069-a7866238c169 (x86_64)
-
- Contained debug information:
- > symtab, debug
-```
-
-Debuggers look for debug information files in various locations when they attach to a process. Search locations include the folder of the executable or library, paths specified in its meta information, or well-known conventional locations defined by the debugger. Sentry, of course, does not have access to those locations on our user’s machines, so we had to implement other mechanisms to retrieve these files.
-
-## Symbol servers
-
-When we first introduced native crash handling at Sentry, we added the ability to [upload debug information files](https://docs.sentry.io/cli/dif/) for server-side symbolication. This can be done as part of the build process or CI pipeline to ensure that all relevant information is available to Sentry once crash reports are coming in.
-
-With our newly announced [support for symbol servers](https://blog.sentry.io/2019/05/23/native-crash-reporting-symbol-servers-pdbs-sdk-c-c-plus-plus), we have now added a second, more convenient way to provide debug information. Instead of uploading, Sentry will download debug files as needed.
-
-When implementing this feature, we realized just how inconsistent debug file handling still is. While Microsoft has established a de facto standard for addressing PDBs, all other platforms are still very underspecified. In total, we have implemented 5 different schemas for addressing debug information files on symbol servers:
-
-- Microsoft SymbolServer (including compression)
-- [SSQP](https://github.com/dotnet/symstore/blob/master/docs/specs/SSQP_Key_Conventions.md) (Simple Symbol Query Protocol)
-- Google Breakpad’s Directory Layout
-- [LLDB File Mapped UUID Directories](http://lldb.llvm.org/use/symbols.html#file-mapped-uuid-directories)
-- [GDB Build ID Method](https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html)
-
-While some of them are quite similar, they all handle certain file types differently or have their own formatting for the file identifiers. As we’re also continuing to expand our internal repositories of debug files, we will be working towards a more accessible and consistent standard that covers all major platforms to avoid issues like case insensitivity during lookup.
-
-## Symbolication as a service
-
-Since the beginning, it has been our objective to create reusable components for the handling of debug information and native symbolication in general. For a long time, most of our efforts have concentrated on creating and contributing to Rust libraries. Aside from being a language predestined for a task like this, Rust’s ecosystem has been growing rapidly over the past years to provide an incredible set of tools for quick development in this space.
-
-Over the past months, we have started to move a lot of the symbolication code that we have been using at Sentry into a standalone service. We’re now proud to present [Symbolicator](https://github.com/getsentry/symbolicator), a standalone native symbolication service. (Outstanding name, right?)
-
-Symbolicator can process native stack traces and minidumps and will soon learn more crash report formats. It uses symbol servers to download debug files and cache them intelligently for fast symbolication. Symbolicator also comes with a scope isolation concept built-in so that it can be used in multi-tenant use cases. Over time, we will be adding more capabilities and tools around debug file handling.
-
-Additionally, Symbolicator can act as a symbol server proxy. Its API is compatible with Microsoft’s symbol server, which means you can host your own instance and point Visual Studio to it. Symbolicator will automatically serve debug files from configured sources like S3, GCS or any other available symbol server.
-
-Symbolicator is and will always be 100% open-source. For now, it can be built from source, and we will soon start to publish binary releases. [Stop by](https://github.com/getsentry/symbolicator), and feel free to open an issue in the [issue tracker](https://github.com/getsentry/symbolicator/issues).
diff --git a/src/content/blog/building-type-safe-metrics-api-in-swift-part-i.md b/src/content/blog/building-type-safe-metrics-api-in-swift-part-i.md
deleted file mode 100644
index 18a01be0..00000000
--- a/src/content/blog/building-type-safe-metrics-api-in-swift-part-i.md
+++ /dev/null
@@ -1,317 +0,0 @@
----
-title: "Building Type-Safe Metrics API in Swift: Part I"
-date: "2026-02-09T09:00:00Z"
-tags: ["swift", "sdk", "apple", "cocoa", "ios", "macos", "metrics"]
-summary: "Explore protocol extensions, enums with associated values, and ExpressibleByStringLiteral to build type-safe Swift APIs."
-images: ["../../assets/images/building-type-safe-metrics-api-in-swift/hero.png"]
-postLayout: PostLayout
-canonicalUrl: building-type-safe-metrics-api-in-swift-part-i
-authors: ["philniedertscheider"]
----
-
-With the release of [Apple / Cocoa SDK v9.4.0](https://github.com/getsentry/sentry-cocoa/releases/tag/9.4.0), we're excited to share not just the new experimental Metrics feature, but the engineering thinking behind it.
-
-Already available in our [Python](https://docs.sentry.io/platforms/python/metrics/), [JavaScript](https://docs.sentry.io/platforms/javascript/guides/node/metrics/), [Flutter](https://docs.sentry.io/platforms/dart/guides/flutter/metrics/) and [many more SDKs](https://docs.sentry.io/product/explore/metrics/getting-started/#supported-sdks), [Metrics](https://docs.sentry.io/product/explore/metrics/) let you collect custom measurements to gain deeper insights into your app:
-
-```swift
-// Track how many users completed checkout
-SentrySDK.metrics.count(
- key: "checkout.completed",
- value: 1,
- attributes: [
- "payment_method": "apple_pay",
- "cart_items": 3
- ]
-)
-
-// Monitor your in-memory cache size
-SentrySDK.metrics.gauge(
- key: "cache.size_mb",
- value: 42.5,
- attributes: [
- "cache_name": "image_cache"
- ]
-)
-
-// Measure how long image processing takes
-SentrySDK.metrics.distribution(
- key: "image.processing_time",
- value: 187.5,
- unit: .millisecond
-)
-```
-
-While the proof of concept was done weeks ago, most of our effort went into designing the public API - the interface our SDK users interact with daily, and one we can't easily change once released.
-
-In this two-part series, I'll walk you through our design process and the Swift features that made it possible.
-
-In **Part I** (this post), we'll cover:
-
-- **Protocol extensions** as the Swift feature designed for adding default values to protocol methods
-- Enums with **associated values** for extended customization
-- Using **`ExpressibleByStringLiteral`** to convert literals straight into types
-
-In [**Part II**](/blog/building-type-safe-metrics-api-in-swift-part-ii), we'll dive deeper into:
-
-- Replacing `Any` with type-safe attribute values
-- Handling Swift compiler limitations with array conformance
-- **Forward-compatible enum** design using `@unknown default` and `@frozen`
-
-Join me on this deep dive and let's get straight into it.
-
-## Three Important Methods
-
-From a user perspective, the most important parts are the **methods used to capture metrics**.
-To enable this capability, the SDK needs to offer a `SentrySDK.metrics` object with the three static methods `.count(..)`, `.gauge(..)` and `.distribution(..)`, each with a `key` and `value` parameter.
-
-With that the first language feature came into play, as we decided against surfacing a concrete type (e.g. a `class`), and instead adopt it using a [protocol](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols) (also known as "interfaces" in other programming languages).
-This allows us to easily refactor otherwise public types, reducing the need for breaking changes in later versions of the SDK.
-
-For the value type we use `Double` for the gauge and distribution metrics to capture values with floating point precision, including negative values.
-But for counter metrics we realized that the count is always a **whole number** and **never negative**, resulting in the decision of using unsigned integers `UInt` for them.
-
-```swift
-protocol SentryMetricsApiProtocol {
- func count(key: String, value: UInt)
- func distribution(key: String, value: Double)
- func gauge(key: String, value: Double)
-}
-```
-
-### Omit Parameter With Default Values
-
-Looking at [our technical specifications for Metrics](https://develop.sentry.dev/sdk/telemetry/metrics/#trace_metric-envelope-item-payload) we notice one detail in the requirements:
-
-> For `counter` metrics: the count to increment by **(should default to 1)**
-
-This means it must be possible for SDK users to capture a counter metric without having to explicitly define a `value` in the method call, falling back to `1` as a default.
-Commonly, this is solved by using a default value in the method signature, e.g., `func count(key: String, value: UInt = 1)` allowing an invocation with `count(key: "my-key")` and `count(key: "my-key", value: 123)`.
-
-In our case Swift's protocols do not support default values directly in their definitions, which results in a build-time error:
-
-
-
-This is exactly the use case **[Protocol Extensions](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/#Protocol-Extensions)** are designed for.
-
-[Extensions](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/extensions) in Swift allow adding additional logic to types, e.g. if a data type `struct` has a getter for `firstName` and `lastName`, an extension could add `fullName` returning the concatenation of the two strings.
-
-```swift
-struct Person {
- let firstName: String
- let lastName: String
-}
-
-extension Person {
- var fullName: String {
- firstName + " " + lastName
- }
-}
-```
-
-The important part to understand here is that protocols can also be extended, but the extensions only know about the signature of the protocol itself, therefore we can also only access methods defined in `SentryMetricsApiProtocol`.
-To our luck this is actually all we need, as we are adding convenience overloads for our methods, allowing callers to omit the optional parameters:
-
-```swift
-protocol SentryMetricsApiProtocol {
- // ❌ Requires `value` to always be set
- func count(key: String, value: UInt)
-}
-
-extension SentryMetricsApiProtocol {
- // ✅ Allows calling method without setting `value`
- func count(key: String, value: UInt = 1) {
- // Call the actual implementation of the protocol
- self.count(key: key, value: value)
- }
-}
-```
-
-Great, now that we have our public API established with a default value for counters, it's time to extend it with the next useful addition: **metrics units**.
-
-## Metrics Units, Enums And Generic Values
-
-Sentry's telemetry system has a standardized [list of pre-defined units](https://develop.sentry.dev/sdk/telemetry/attributes/#units) which will eventually enable further server-side aggregation and data processing.
-
-The simplest solution would be changing the API to offer an additional parameter of type `String` to define the unit.
-But, as these are standardized across SDKs, we can also use Swift's [`enum`](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations) type to offer compile-time safety and by defining the raw value as `String`, the compiler takes care of generating String values for each case and other boilerplate code for us:
-
-```swift
-enum SentryUnit: String {
- case nanosecond
- case microsecond
- case millisecond
-
- // ... and more!
-}
-
-// Example:
-let unit = SentryUnit.nanosecond
-
-// When the compiler can infer the type of a variable, we don't
-// need to explicitly define it again on the right-hand side:
-let unit: SentryUnit = .nanosecond
-```
-
-As the `unit` parameter is optional and should also be omittable, we can leverage our protocol extension once again to implement it:
-
-```swift
-protocol SentryMetricsApiProtocol {
- func count(key: String, value: UInt)
- func distribution(key: String, value: Double, unit: SentryUnit?)
- func gauge(key: String, value: Double, unit: SentryUnit?)
-}
-
-extension SentryMetricsApiProtocol {
- func count(key: String, value: UInt = 1) {
- self.count(key: key, value: value)
- }
-
- func distribution(key: String, value: Double, unit: SentryUnit? = nil) {
- self.distribution(key: key, value: value, unit: unit)
- }
-
- func gauge(key: String, value: Double, unit: SentryUnit? = nil) {
- self.gauge(key: key, value: value, unit: unit)
- }
-}
-
-// Value falls back to 1
-SentrySDK.metrics.count(key: "network.request.count")
-
-// Value is explicitly set to 2
-SentrySDK.metrics.count(key: "memory.warning", value: 2)
-
-// Distribution with value and unit
-SentrySDK.metrics.distribution(key: "queue.processed_bytes", value: 512.0, unit: .bytes)
-```
-
-So, how about using non-standard units?
-
-While using an enum as a type-safe approach of constants, we lost a big advantage compared to pure `String` constants, as we are now **not able to pass custom/generic units** in the method calls anymore.
-The method typing is strict, so if we pass in a parameter `unit`, it must be a `SentryUnit`.
-
-This is where [Swift's Associated Values](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations#Associated-Values) come into play, allowing us to keep using well-known enum types, but extending our new type `generic` with an associated custom `String` value:
-
-```swift
-public enum SentryUnit {
- case nanosecond
- case generic(String)
-}
-
-let unit = SentryUnit.generic("custom unit")
-```
-
-Unfortunately, this change requires us to remove the [raw value conformance](https://developer.apple.com/documentation/Swift/RawRepresentable#Enumerations-with-Raw-Values), resulting in the loss of compiler generated serialization:
-
-
-
-But, this minor inconvenience can easily be resolved by implementing manual conformance to the Swift standard library's [`RawRepresentable`](https://developer.apple.com/documentation/Swift/RawRepresentable) protocol, with all unknown unit types converting from or to the enum type `generic`:
-
-```swift
-extension SentryUnit: RawRepresentable {
- /// Maps known unit strings to their corresponding enum cases, or falls back to `.generic(rawValue)` for any unrecognized string (custom units).
- init?(rawValue: String) {
- switch rawValue {
- case "nanosecond":
- self = .nanosecond
- default:
- self = .generic(rawValue)
- }
- }
-
- /// Returns the string representation of the unit.
- public var rawValue: String {
- switch self {
- case .nanosecond:
- return "nanosecond"
- case .generic(let value):
- return value
- }
- }
-}
-```
-
-Now it's easy to add more information to our metrics, e.g. by using a custom unit type `"tasks"`:
-
-```swift
-SentrySDK.metrics.gauge(
- key: "queue.depth",
- value: 42.0,
- unit: .generic("tasks")
-)
-```
-
-### Syntactic Sugar for Custom Units
-
-Looking at the usage of the generic unit as in `unit: .generic("custom")` raises the question of how we can reduce boilerplate code.
-We already know that if we don't use any of the pre-defined constants like `.nanosecond`, we **always** have a String value that should **always** be seen as a "generic" / "custom" unit (Yes, _always_ is bold twice on purpose).
-
-```swift
-// ⚠️ Not ideal having to use `.generic()` every time
-SentrySDK.metrics.gauge(
- key: "queue.depth",
- value: 42.0,
- unit: .generic("items")
-)
-
-// ✅ Clean and compact
-SentrySDK.metrics.gauge(
- key: "queue.depth",
- value: 42.0,
- unit: "items"
-)
-```
-
-If wrapping it in `SentryUnit.generic(..)` (or just `.generic(..)` using compiler [type-inference](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/#Type-Safety-and-Type-Inference)) every single time seems like repetitive boilerplate code to you, there's something we can do about it!
-
-As a final cherry-on-top improvement opportunity for generic units, we adopt the protocol [`ExpressibleByStringLiteral`](https://developer.apple.com/documentation/swift/expressiblebystringliteral) for our enum `SentryUnit`.
-This protocol of the Swift standard library is baked into the compiler and requires us to define an additional initializer:
-
-```swift
-extension SentryUnit: ExpressibleByStringLiteral {
- init(stringLiteral value: StringLiteralType) {
- self = .generic(value)
- }
-}
-```
-
-This small extension indicates to the compiler that literal `String` values can directly be converted into enums:
-
-```swift
-// ✅ Compiler converts the string to an enum with associated value
-let unit: SentryUnit = "items"
-
-// ❌ Does not work for String variables, only literal values
-let myUnit = "some value"
-let unit: SentryUnit = myUnit
-
-// ✅ String variables still need to be wrapped
-let unit: SentryUnit = .generic(myUnit)
-```
-
-All of these additions now result in an even cleaner API with custom metric units, while still supporting pre-defined constants.
-
-Note that generic/custom units are currently not supported by Sentry's data processing, but we designed the API this way for forward compatibility. Once Relay/Sentry supports generic/custom units, your code will work without requiring an SDK upgrade.
-
-## What's Next
-
-We've now established a clean API for capturing metrics with type-safe units. But our journey isn't over yet.
-
-The real challenge comes when we add **attributes** — key-value pairs that provide context to your metrics — and how to accept multiple value types (`String`, `Int`, `Bool`, `Array`, etc.) without falling back to type-erased `Any`.
-
-In [**Part II**](/blog/building-type-safe-metrics-api-in-swift-part-ii), we'll tackle:
-
-- Why `Any` leads to unusable data
-- Building a protocol-based "union-like type" for attribute values
-- Navigating Swift compiler limitations with array conformance
-- Future-proofing enums with `@unknown default` and `@frozen`
-
-**[Continue to Part II →](/blog/building-type-safe-metrics-api-in-swift-part-ii)**
-
-In the meantime, the Metrics API is now available in [sentry-cocoa v9.4.0](https://github.com/getsentry/sentry-cocoa/releases/tag/9.4.0):
-
-- **Want to see how we implemented it?** The [full source code](https://github.com/getsentry/sentry-cocoa/) is open source
-- **Found a bug or have feedback?** [Open an issue](https://github.com/getsentry/sentry-cocoa/issues/new) on GitHub
-- **Interested in building developer tools?** We're hiring - [check out our open positions](https://sentry.io/careers)
-
-Feel free to reach out on [X](https://x.com/philprimes) or [Bluesky](https://bsky.app/profile/philprime.dev) with your thoughts, questions, or your own Swift API design stories.
diff --git a/src/content/blog/building-type-safe-metrics-api-in-swift-part-ii.md b/src/content/blog/building-type-safe-metrics-api-in-swift-part-ii.md
deleted file mode 100644
index 9a77fd88..00000000
--- a/src/content/blog/building-type-safe-metrics-api-in-swift-part-ii.md
+++ /dev/null
@@ -1,535 +0,0 @@
----
-title: "Building Type-Safe Metrics API in Swift: Part II"
-date: "2026-02-09T10:00:00Z"
-tags: ["swift", "sdk", "apple", "cocoa", "ios", "macos", "metrics"]
-summary: "Replace Any with type-safe protocols, handle array conformance limitations, and future-proof your Swift enums."
-images: ["../../assets/images/building-type-safe-metrics-api-in-swift/hero.png"]
-postLayout: PostLayout
-canonicalUrl: building-type-safe-metrics-api-in-swift-part-ii
-authors: ["philniedertscheider"]
----
-
-_This is part II of a two-part series on designing type-safe Swift APIs. If you haven't read [Part I](/blog/building-type-safe-metrics-api-in-swift-part-i) yet, I highly recommend starting there, as we covered protocol extensions for default values, enums with associated values, and `ExpressibleByStringLiteral` for cleaner syntax._
-
-
-
-In [Part I](/blog/building-type-safe-metrics-api-in-swift-part-i), we built the foundation of our Metrics API: type-safe methods with optional parameters and flexible unit types.
-Now it's time to add our last parameter to the public methods: **Attributes**.
-
-[Attributes](https://develop.sentry.dev/sdk/telemetry/attributes/) are a list of key-value pairs with a `String` as a key and a value of one of our supported data types. In this post, we'll explore:
-
-- Why using `Any` for attribute values leads to unusable data
-- How to build a protocol-based "union type" that only accepts valid values
-- Navigating Swift compiler limitations with array conformance
-- Future-proofing your enums with `@unknown default`
-
-Let's dive in.
-
-## Adding Context With Attributes
-
-At the time of writing this blog, these are the value types supported by Sentry's data processing:
-
-- `string`
-- `boolean`
-- `integer` (64-bit signed integer)
-- `double` (64-bit floating point number)
-- `array` (single type, but mixed types in the future)
-
-Attributes are not a new addition to the SDK, as they're already used by the [Logs feature](https://docs.sentry.io/product/explore/logs/) released with [v8.54.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.54.0).
-
-During the initial implementation of logging, we decided to adopt a generic type [`Any`](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/typecasting/#Type-Casting-for-Any-and-AnyObject) for the value of the attributes, allowing us to include all of the supported types, while also being compatible with Objective-C.
-
-```swift
-// Source: https://github.com/getsentry/sentry-cocoa/blob/09a80f2770eaf5d8e6fc34a33a4e8e6939393d0a/Sources/Swift/Tools/SentryLogger.swift
-@objc(info:attributes:)
-public func info(_ body: String, attributes: [String: Any]) {
- // Convert provided attributes to SentryLog.Attribute format
- var logAttributes = attributes.mapValues { SentryLog.Attribute(value: $0) }
-
- // Create and capture a full log entry
- let log = SentryLog(
- timestamp: dateProvider.date(),
- traceId: SentryId.empty,
- level: level,
- body: SentryLogMessage(stringLiteral: body),
- attributes: logAttributes
- )
- delegate.capture(log: log)
-}
-```
-
-The type `SentryLog.Attribute` is actually a [typealias](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/#Type-Aliases) for the [`SentryAttribute`](https://github.com/getsentry/sentry-cocoa/blob/142ed2ca1101e982f17fef4874fe94eb3cae880a/Sources/Swift/Protocol/SentryAttribute.swift) which is a class type holding a String identifier `type` and a type-erased property `value`.
-
-This works as expected, but requires a lot of manual type-erasing and type-casting, so when it came to designing the new Swift-only Metrics API, we started again from scratch.
-
-During the first review discussions we considered the idea of using an array of `SentryAttribute` as the parameter, which got scratched immediately because we would not benefit from compile-time checking for duplicate key literal values, which we get when using the dictionary:
-
-```swift
-// Definition:
-func count(key: String, value: UInt, attributes: [SentryAttribute])
-
-// Usage with array of attributes
-SentrySDK.metrics.count(
- key: "network.request.count",
- value: 1,
- attributes: [
- SentryAttribute(key: "endpoint", value: "/api/users"),
- SentryAttribute(key: "endpoint", value: "/api/users/123"), // ❌ This would compile
- ]
-)
-
-// Usage with dictionary of attribute values
-SentrySDK.metrics.count(
- key: "network.request.count",
- value: 1,
- attributes: [
- "endpoint": "/api/users",
- "endpoint": "/api/users/123", // ✅ Will not compile
- ]
-)
-```
-
-This was enough reason to decide that we still want to have a dictionary of `String` keys with associated values supporting multiple types.
-
-But do we really want to have type-erased value types? Can't we use Swift to define a list of types possible for the value of the attributes?
-
-### Understanding The Problem Of Any
-
-As a first step to find a solution, we need to understand our problem.
-
-One major drawback of using `Any` as the value of our attributes is missing compile-time hints if the passed-in value is not one of our supported attribute value types.
-
-To visualize this, take a look at the following example from the Logs API, where we set a `String`, an `Int`, a `Double` and a custom class type instance as attributes:
-
-```swift
-class User {
- let id = "user_123"
- let name = "Jane"
-}
-let currentUser = User()
-
-SentrySDK.logger.info("Purchase completed", attributes: [
- "product_name": "Premium Plan",
- "price": 99,
- "discount_percent": 15.5,
- "user": currentUser // Oops - passing the whole object
-])
-```
-
-This is valid code which will compile, because using type-erased `Any` for the value will allow passing **anything**.
-As a fallback for unknown types such as `User`, we are performing an internal conversion to `String`, resulting in the following serialized data sent to Sentry:
-
-```json
-{
- "severity_number": 9,
- "body": "Purchase completed",
- "attributes": {
- "product_name": {
- "value": "Premium Plan",
- "type": "string"
- },
- "price": {
- "value": 99,
- "type": "integer"
- },
- "discount_percent": {
- "value": 15.5,
- "type": "double"
- },
- "user": {
- "value": "MyApp.MyApp.(unknown context at $103d12130).(unknown context at $103d1213c).User",
- "type": "string"
- }
- }
-}
-```
-
-I believe it's obvious for all readers that `MyApp.MyApp.(unknown context at $103d12130).(unknown context at $103d1213c).User` is pretty much useless as an attribute value.
-Even worse, the `$103d12130` and `$103d1213c` are actually memory addresses, so they will be different with every attribute sent, making it non-deterministic and unusable for querying.
-
-One variant to improve this is adopting the protocol [`CustomStringConvertible`](https://developer.apple.com/documentation/Swift/CustomStringConvertible), requiring us to implement the `description` getter method (similar to `toString()` in other programming languages):
-
-```swift
-class User: CustomStringConvertible {
- let id = "user_123"
- let name = "Jane"
-
- var description: String {
- return ""
- }
-}
-```
-
-This example then serializes to a more useful payload:
-
-```json
-{
- "user": {
- "value": "",
- "type": "string"
- }
-}
-```
-
-This looks already way better, as the memory addresses are now gone, and we can actually see the values themselves. But this already raised the next concerns:
-
-- Does every type now need to adopt `CustomStringConvertible` just in case I accidentally use it as a value?
-
-Yes, in case you keep using class types as attribute values, they need to adopt the protocol; otherwise, we get the memory addresses back. And yes, this is inconvenient.
-
-- Do we really want multiple values in a single attribute?
-
-No, you most likely do not want this, as you want attribute values to be simple and deterministic in meaning, so you can easily write queries in Sentry and explore your data.
-Having them in the same attribute brings in complexity for querying, both for you and for us at Sentry, so generally speaking, it's easier to split them up.
-
-- So if I shouldn't do this, why can't the compiler tell me that I am using a type which will require a fallback, and maybe even produce garbage value data?
-
-That's the exact question we asked ourselves too, resulting in us adopting more Swift language features as you can see in the next sections of this blog post.
-
-### One Type To Rule Them All
-
-As a first step we use the same approaches as described in our previous post for `SentryUnit` by introducing an enum with associated values: `SentryAttributeContent`.
-
-(P.S. there were many rounds of renamings, from "value" to "content" etc., but we decided on this one simply because naming is hard).
-
-```swift
-enum SentryAttributeContent {
- case string(String)
- case boolean(Bool)
- case integer(Int)
- case double(Double)
- case stringArray([String])
- case booleanArray([Bool])
- case integerArray([Int])
- case doubleArray([Double])
-}
-
-protocol SentryMetricsApiProtocol {
- func count(key: String, value: UInt, attributes: [String: SentryAttributeContent])
-}
-
-SentrySDK.metrics.count(key: "checkout.completed", value: 1, attributes: [
- "payment_method": .string("apple_pay"),
- "cart_items": .integer(3),
- "total_amount": .double(99.99)
-])
-```
-
-This is already way better than using `Any`, because now we can only pass in attribute values which are defined as known associated value types of our enum.
-
-So, are we ready to ship? 🚀
-Not quite yet, because just a bit more engineering and we realize that while our protocol allows `Double` values, it does not allow `Float` values, leaving us with an ugly conversion like this:
-
-```swift
-let latency: Float = 123.456
-SentrySDK.metrics.distribution(key: "network.latency", value: 123, attributes: [
- "body_size": .double(Double(latency))
-])
-```
-
-On top of that, we now have, once again, like in the `SentryUnit`, growing boilerplate code, requiring us to convert our variables and literals to enum values every single time.
-
-So what's the Swift-y way to handle this? Exactly! One ~type~ **protocol** to rule them all.
-
-```swift
-protocol SentryAttributeValue {
- var asSentryAttributeContent: SentryAttributeContent { get }
-}
-
-protocol SentryMetricsApiProtocol {
- func count(key: String, value: UInt, attributes: [String: any SentryAttributeValue])
-}
-```
-
-With this new protocol, we change the method signature of our public API once again and now it's using the [`any` keyword](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/opaquetypes/#Boxed-Protocol-Types) instead of a concrete type for the attribute value.
-Due to this change it now accepts all types which adopted the protocol `SentryAttributeValue`, therefore declaring that they have a getter method or property to represent themselves as `SentryAttributeContent` enum value.
-
-Now **every** type can define itself as being representable as one of our supported types, especially types available in the Swift standard library, but also your custom types like `User`:
-
-```swift
-extension String: SentryAttributeValue {
- var asSentryAttributeContent: SentryAttributeContent {
- return .string(self)
- }
-}
-
-extension Bool: SentryAttributeValue {
- var asSentryAttributeContent: SentryAttributeContent {
- return .boolean(self)
- }
-}
-
-extension Int: SentryAttributeValue {
- var asSentryAttributeContent: SentryAttributeContent {
- return .integer(self)
- }
-}
-
-extension Double: SentryAttributeValue {
- var asSentryAttributeContent: SentryAttributeContent {
- return .double(self)
- }
-}
-
-extension Float: SentryAttributeValue {
- var asSentryAttributeContent: SentryAttributeContent {
- return .double(Double(self)) // ✅ Float-to-Double conversion is hidden away
- }
-}
-
-class User: SentryAttributeValue {
- let id = "user_123"
-
- var asSentryAttributeContent: SentryAttributeContent {
- return .string(id) // ✅ Custom types can represent themselves as supported content types
- }
-}
-```
-
-These extensions are part of the SDK and available by default, therefore everyone can now use the Metrics API using variables and literals in attributes:
-
-```swift
-let paymentMethod = "apple_pay" // ✅ Variables work as expected
-SentrySDK.metrics.count(
- key: "checkout.completed",
- value: 1,
- attributes: [
- "payment_method": paymentMethod,
- "cart_items": 3, // ✅ Integer literals just work
- "is_first_purchase": true // ✅ Booleans too
- ]
-)
-```
-
-### Encountering Compiler Limitations
-
-You might have noticed that I did not mention the support of `Array` much yet. That's due to array handling being quite complex, so I want to dedicate this section to it.
-
-As we have established already, we need to extend `Array` so it also adopts and implements the method of `SentryAttributeValue`, but for the best user experience, we want to extend it **only if the array contains elements which are one of our supported types**.
-
-The initial approach was using [extension with a generic where-clause](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/#Extensions-with-a-Generic-Where-Clause) like `extension where ` to add logic to a `TYPE` only if a `CONDITION` on the typing is fulfilled.
-
-```swift
-extension Array: SentryAttributeValue where Element == Int {
- var asSentryAttributeContent: SentryAttributeContent {
- .integerArray(self)
- }
-}
-```
-
-While this works if we write the extension only for a single type, we started to hit compiler errors with multiple type extensions:
-
-
-
-Bummer! We can't have multiple conformances of the same protocol scoped to specific element types.
-Luckily we already introduced `SentryAttributeValue` as our "union" of supported types which can be applied here:
-
-```swift
-extension Array: SentryAttributeValue where Element == SentryAttributeValue {
- var asSentryAttributeContent: SentryAttributeContent {
- if Element.self == Bool.self, let values = self as? [Bool] {
- return .booleanArray(values)
- }
- // ... and other cases
-
- // Fallback to converting to strings
- return .stringArray(self.map { element in
- String(describing: element)
- })
- }
-}
-```
-
-For the sake of readability of this blog post I am not going to embed the entire casting logic here, so if you want to see it in detail, all of [our source code is open source](https://github.com/getsentry/sentry-cocoa/blob/142ed2ca1101e982f17fef4874fe94eb3cae880a/Sources/Swift/Protocol/SentryAttributeValue.swift#L91) for you to check out.
-
-This worked well (for a while), as we were now able to pass in arrays of `String` , arrays of `Bool`, etc. for all the types which adopted `SentryAttributeValue`:
-
-```swift
-SentrySDK.metrics.count(
- key: "order.placed",
- attributes: [
- "customer_id": "cust_456", // ✅ String works
- "product_ids": ["sku_1", "sku_2"], // ✅ Array of String works
- "quantities": [2, 1, 3] // ✅ Array of Integer works too
- ]
-)
-```
-
-But there was already another pattern becoming visible: all of the arrays are homogeneous to a single type, therefore they were not actually arrays of `SentryAttributeValue`, but arrays of types adopting `SentryAttributeValue`.
-
-It's a thin line in definition, which surfaced a challenge when mixing multiple types adopting `SentryAttributeValue` into a single array, which we could not prohibit from happening.
-We hoped that the compiler would somehow be smart enough to understand that now it's an array of `SentryAttributeValue`, but instead it fell back to an array of `Any`.
-
-```swift
-struct ProductID: SentryAttributeValue {
- var asSentryAttributeContent: SentryAttributeContent {
- return .string("product_1")
- }
-}
-
-struct CategoryID: SentryAttributeValue {
- var asSentryAttributeContent: SentryAttributeContent {
- return .string("electronics")
- }
-}
-
-SentrySDK.metrics.count(
- key: "page.viewed",
- attributes: [
- // Mixed array of types adopting SentryAttributeValue
- // Both return string content, so this could be a string[]
- // ❌ Compiler sees [Any], not [SentryAttributeValue], and fails
- "related_items": [ProductID(), CategoryID()]
- ]
-)
-```
-
-As `Any` is a type which cannot be extended nor does it have a clear representation as an attribute value, we had to remove the condition from the Array extension and add additional casting:
-
-```swift
-extension Array: SentryAttributeValue { // ✅ removed the where-clause
- var asSentryAttributeContent: SentryAttributeContent {
- if Element.self == Bool.self, let values = self as? [Bool] {
- return .booleanArray(values)
- }
- // ... and other cases
- if let values = self as? [SentryAttributeValue] {
- return castArrayToAttributeContent(values: values)
- }
- // Fallback to converting to strings
- return .stringArray(self.map { element in
- String(describing: element)
- })
- }
-}
-```
-
-This was the final solution which now casts from arrays of `Any` to our known types, including handling of other types adopting the protocol, and a fallback to arrays of `String` for everything else.
-
-### Granular Control
-
-As [it is common](https://develop.sentry.dev/sdk/expected-features/#before-send-hook) in our Sentry SDKs, we want to allow our users to be able to manually filter and manipulate collected metric items for data enrichment, data scrubbing, and other use cases, before they are sent to Sentry.
-
-This was also decided for the Metrics feature, so we introduced the option [`beforeSendMetric`](https://develop.sentry.dev/sdk/telemetry/metrics/#initialization-options), which is a _"[..] function that takes a metric object and returns a metric object [..] called before sending the metric to Sentry"_.
-
-To embrace the Swift-iness of our implementation we also reconsidered the need for using [`class`-based reference type instances](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/classesandstructures/) for the metrics objects.
-Instead, they should be handled as immutable data inside of the SDK and only be transformed/mapped if needed.
-We decided to use `struct` data types with `SentryMetric` as our parameter type and `SentryMetric?` as a nullable return type.
-
-While this removes compatibility with Objective-C (as `struct` is Swift-only), the metric is passed as an immutable copy to the `beforeSendMetric` closure and cannot be modified directly, unless it's copied to a local variable first.
-We also considered passing it in as an [`inout`](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/functions/#In-Out-Parameters) parameter to allow modification via a reference, but decided against it because it would require us to change the input parameter to be nullable too (which bad practice as it is never the case).
-
-For the type of the `attributes` property of the metric, we decided to expose the dictionary values **not using** `SentryAttributeValue` as in the capturing methods, but instead directly the enum `SentryAttributeContent`.
-This allows you to identify and modify the typed metrics using `switch` for multi-case or `if case` for single-case handling.
-
-Bringing it all together the `beforeSendMetric` can now be used like this:
-
-```swift
-// Experimental for now, will be a top-level option in the future
-class SentryExperimentalOptions {
- var beforeSendMetric: ((Sentry.SentryMetric) -> Sentry.SentryMetric?)?
-}
-
-options.experimental.beforeSendMetric = { metric in
- // Create a mutable copy (SentryMetric is a struct)
- var metric = metric
-
- // Drop metrics with specific attribute values set
- if case .boolean(let dropMe) = metric.attributes["dropMe"], dropMe {
- return nil
- }
-
- // Modify metric attributes using literals converted to our enum types
- metric.attributes["processed"] = true
- metric.attributes["processed_at"] = "2024-01-01"
-
- return metric
-}
-```
-
-### Forwards-Compatibility
-
-During one of our review discussions we encountered an interesting edge case with regards to forward compatibility:
-
-When using an `enum` in a [`switch`](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/controlflow/#Switch) case matching, it is necessary to handle either all cases, or to define a `default` case to match the unhandled ones:
-
-```swift
-// Example type with subset of all supported types
-enum Value {
- case boolean(Bool)
- case integer(Int)
- case string(String)
-}
-
-// Default case for unhandled ones
-switch value {
-case .boolean(let val):
- // val is true or false
-default: // ⚠️ required
- // do nothing
-}
-
-// Handle all cases
-let value: Value = ...
-switch value {
-case .boolean(let val):
- // val is true or false
-case .integer(let val):
- // val is an integer
-case .string(let val):
- // val is a String
-
-// default: ✅ not necessary
-}
-```
-
-The important aspect here is that the `enum` is defined in our SDK, therefore it can always happen that we want to implement a new type, e.g. `float`, in a future release.
-Now if an SDK user handles all cases of the attribute value, therefore not having to add a `default` statement, it could result in unhandled cases.
-
-But the Swift compiler developers considered this by offering the `@unknown default` case which may be added for Swift 5 projects, and must be added when using Swift 6:
-
-
-
-```swift
-let value: Value = ...
-switch value {
-case .boolean(let val):
- // val is true or false
-case .integer(let val):
- // val is an integer
-case .string(let val):
- // val is a String
-@unknown default:
- // ✅ handles all future cases
-}
-```
-
-One alternative is attributing our enum as [`@frozen`](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/attributes/#frozen), indicating that the enum will never change in future versions.
-While it makes sense for enums like e.g. `CoordinateAxis` having only `vertical` and `horizontal` axes and never anything else, it's not suitable for our evolving protocol definitions.
-
-## Conclusion
-
-Across this two-part series, we've explored how Swift's type system can transform API design from "hope it works" to "guaranteed to work."
-
-The result is a Metrics API where:
-
-- Invalid values won't compile, catching mistakes before they ship
-- The compiler autocompletes exactly what you need
-- Custom types are first-class citizens
-- Future SDK updates won't break your code
-
-But every innovation comes with trade-offs: This API is Swift-only, so Objective-C projects can't use it directly right now (though you can create a wrapper).
-We're already working on [an Objective-C companion](https://github.com/getsentry/sentry-cocoa/issues/6342) for a future release, so keep an eye on that.
-
-In the end, we believe this is the direction Swift SDKs should go: **making the right thing easy and the wrong thing impossible**.
-
-## Try It Out
-
-The Metrics API is now available in [sentry-cocoa v9.4.0](https://github.com/getsentry/sentry-cocoa/releases/tag/9.4.0) and we'd love to hear what you think:
-
-- **Found a bug or have feedback?** [Open an issue](https://github.com/getsentry/sentry-cocoa/issues/new) on GitHub
-- **Want to see how we implemented it?** The [full source code](https://github.com/getsentry/sentry-cocoa/) is open source
-- **Interested in building developer tools?** We're hiring - [check out our open positions](https://sentry.io/careers)
-
-If you made it this far, you're exactly the kind of developer who appreciates well-designed APIs.
-Feel free to reach out on [X](https://x.com/philprimes) or [Bluesky](https://bsky.app/profile/philprime.dev) with your thoughts, questions, or your own Swift API design stories.
diff --git a/src/content/blog/designing-sentrys-cross-region-replication.md b/src/content/blog/designing-sentrys-cross-region-replication.md
deleted file mode 100644
index 8cc09cdd..00000000
--- a/src/content/blog/designing-sentrys-cross-region-replication.md
+++ /dev/null
@@ -1,160 +0,0 @@
----
-title: "Designing Sentry's cross-region replication"
-date: "2024-06-28"
-tags: ["multiregion", "building sentry"]
-draft: false
-summary: "Cross-region replication is a foundational subsystem in multi-region Sentry. This post explores our design process."
-images:
- [
- "../../assets/images/designing-sentrys-cross-region-replication/scenario-outline.png",
- "../../assets/images/designing-sentrys-cross-region-replication/cdc-kafka.png",
- "../../assets/images/designing-sentrys-cross-region-replication/outbox-sequence.png",
- ]
-postLayout: PostLayout
-authors: ["markstory"]
----
-
-When we started designing multi-region Sentry, we didn’t plan on having replication between regions. However, as we got farther in the design process it became clear that because of where our silo boundaries would be, we would need data replicated between regions to facilitate looking up in which region an organization was in, or validating API tokens. While these operations could be completed with Remote Procedure Calls (RPC). The latency, atomicity and resiliency impacts of these high-volume RPC operations wouldn’t be acceptable, and we needed a solution that would be more efficient and more correct.
-
-As the scenarios where we would use cross-region replication became more clear we collected the following requirements:
-
-- We needed resiliency to network failures. Replication would be disrupted by networking issues so we needed the system to work off of last-known information. During a disruption replicated data could become stale, but wouldn’t expire.
-- We wanted to be able to backlog changes without risk of an in-memory system overflowing and failing.
-- We wanted a solution that could reliably replicate changes eventually. Data loss should be rare, and we shouldn’t need to manually intervene to correct divergent state caused by networking or unavailability in other parts of the system.
-- We wanted a solution that we could gradually integrate into the application and validate replication both in CI, and in production before relying on it for customer traffic.
-- We didn’t want workloads from one customer to impact replication of another customer’s data.
-
-Our requirements led us to looking at solutions that would provide an eventually consistent model that leveraged durable data stores.
-
-## Scenarios where we use cross-region replication
-
-We use cross-region replication between our [region silos, and control silo](https://develop.sentry.dev/architecture/#silo-modes). Replication is performed in both directions but for different operations.
-
-For example, when an organization creates a new API token, that token needs to be present in both the control silo, and in the region where the organization is located.
-
-
-
-Authentication tokens are centralized in Control Silo, so that we can locate the organization tokens, work against control silo resources, and so ensure that all tokens are unique with database constraints. We need to replicate the organization’s token to region silo so that it can be used to authenticate requests made there. Later, when the token is deleted, the user will remove it from Control Silo, and replication should update the relevant region.
-
-We also use cross-region replication to push organization membership from the regions into Control Silo. That allows us to apply membership role permissions to organization resources in Control Silo (like Authentication tokens and Integrations).
-
-# Evaluating existing solutions
-
-With a better understanding of our requirements, and workflows, we began evaluating existing solutions for how much complexity and infrastructure they would require in addition to meeting our functional requirements.
-
-## Postgres Replication
-
-Postgres comes with built-in capabilities to stream data from one database server into a replica that supports read operations. Postgres replication would have met our consistency requirements and would have required the smallest number of application changes. Postgres replication was not selected for a few reasons:
-
-1. Postgres can only replicate at the table level. This would result in data being ‘over-replicated’ to regions. If a US organization creates an Authentication Token, there is no reason for that token to be replicated to other regions.
-2. Postgres replication could not be used for the Region → Control path as a replica cannot have multiple primaries/leaders, and with each region replicating data back to control, we would have that scenario.
-
-### Change Data Capture (CDC) and Kafka
-
-Because naive Postgres replication would have resulted in over-replication, we could build an application that consumed Postgres’ streaming replication data, and converted changes into Kafka messages. A Postgres replication consumer would allow us to selectively replicate changes to only the regions that were relevant, and transform queries into relevant domain actions when creating Kafka messages.
-
-
-
-CDC would provide atomic operations and we could easily backlog operations in Kafka for as long as required. Regions would use [mirror maker](https://developers.redhat.com/articles/2023/11/13/demystifying-kafka-mirrormaker-2-use-cases-and-architecture#use_cases) to replicate topics, or use a [multi-region Kafka](https://docs.confluent.io/platform/current/multi-dc-deployments/multiregion.html)
-
-We decided against this approach because of the complexity. We would need to operate additional Kafka clusters, maintain and operate a WAL consumer, Kafka producers, consumers, and translation code from WAL operations into replication changes.
-
-# Outboxes
-
-[Transactional Outboxes](https://microservices.io/patterns/data/transactional-outbox.html) are a distributed systems pattern that fit our use case perfectly. As the application makes changes that need to be replicated, it can save an ‘outbox’ in the same postgres transaction as the change - providing the atomicity we wanted. In the background, a worker pulls tasks from the outbox table and runs handlers to apply the necessary replication action via RPC. With this design we would be able to provide eventual consistency with at-least-once delivery semantics.
-
-
-
-When delivering an outbox message our implementation assumes that any failures will raise errors, and that if an outbox handler completes successfully, that the replication operation is complete.
-
-## Outbox Storage
-
-In order to reach our customer isolation requirements, we tailored our outbox storage around the ideas of shards and categories. Within the outbox storage we divide messages into multiple shards based on the scope of a message. Message scopes are generally bound to the Organization or User. Messages also have a `category` and `object_identifier` which defines the type of operation being performed, and the record the operation is for.
-
-With these attributes we’re able to process messages for each ‘shard’ in parallel independently from other shards. Furthermore, because messages are delivered idempotently, we coalesce messages with the same `category` and `object_identifier`. Coalescing messages allows us to short cut replication by not transferring all of the intermediary stages.
-
-Unfortunately because of how our Postgres databases are partitioned, we’ve needed to create several outbox tables in order to preserve transactional semantics.
-
-## Delivering outbox messages
-
-When outbox messages are delivered, the outbox delivery system will call all registered handlers for the outbox category. The handler for organization authentication tokens looks like:
-
-```python
-def handle_async_replication(self, region_name: str, shard_identifier: int) -> None:
- from sentry.services.hybrid_cloud.orgauthtoken.serial import serialize_org_auth_token
- from sentry.services.hybrid_cloud.replica import region_replica_service
-
- region_replica_service.upsert_replicated_org_auth_token(
- token=serialize_org_auth_token(self),
- region_name=region_name,
- )
-```
-
-Because all of our outbox handlers use RPC for replication we had to invest the time to make our RPC operations idempotent. A simple way to make operations idempotent is to transfer a snapshot of the current state instead of the changes to a object. By sending the entire object we ensure that state will converge and become consistent.
-
-## Ensuring consistency
-
-Because our consistency relies on outbox messages being created at the same time as record changes, we could easily get into scenarios where application developers forget to create an outbox record when they persist a change to the source record. To prevent this scenario from occurring we did two things.
-
-The first is to update our ORM models so that `.save()` and `.update()` apply both the transaction and outbox message generation. This covers scenarios where we modify records and then persist them but still leaves us vulnerable to missing outboxes created by bulk queries.
-
-For example, the following operation would break cross-region consistency
-
-```python
-from sentry.models import OrganizationMember
-
-OrganizationMember.objects.filter(id=member_id).update(user_id=None)
-```
-
-To handle this scenario, we elected to build tooling that audits the SQL emitted by the application during tests and use a set of heuristics to find queries that could cause consistency issues. The above query would be detected by our test suite tooling and emit the following error:
-
-```
-_______________________________________ ERROR at teardown of OrganizationMemberTest.test_consistency ________________________________________
-tests/conftest.py:104: in audit_hybrid_cloud_writes_and_deletes
- validate_protected_queries(conn.queries)
-src/sentry/testutils/silo.py:520: in validate_protected_queries
- raise AssertionError("\n".join(msg))
-E AssertionError: Found protected operation without explicit outbox escape!
-E
-E UPDATE "sentry_organizationmember" SET "user_id" = NULL WHERE "sentry_organizationmember"."id" = 7
-E
-E Was not surrounded by role elevation queries, and could corrupt data if outboxes are not generated.
-E If you are confident that outboxes are being generated, wrap the operation that generates this query with the `unguarded_write()`
-E context manager to resolve this failure. For example:
-E
-E with unguarded_write(using=router.db_for_write(OrganizationMembership):
-E member.delete()
-E
-E Query logs:
-E
-E SELECT 'start_role_override_30'
-E UPDATE "sentry_organizationmember" SET "user_is_active" = true, "user_email" = 'd1006c8c057a462ba94133e7ac40f488@example.com' WHERE "sentry_organizationmember"."user_id" = 6
-E SELECT 'end_role_override_30'
-E SELECT "sentry_regionoutbox"."id" FROM "sentry_regionoutbox" WHERE ("sentry_regionoutbox"."category" = 3 AND "sentry_regionoutbox"."object_identifier" = 7 AND "sentry_regionoutbox"."shard_identifier" = 4554241598816256 AND "sentry_regionoutbox"."shard_scope" = 0) LIMIT 100
-E DELETE FROM "sentry_regionoutbox" WHERE "sentry_regionoutbox"."id" IN (15)
-E RELEASE SAVEPOINT "s8377039552_x80"
-E SAVEPOINT "s8377039552_x85"
-E SELECT "sentry_regionoutbox"."id", "sentry_regionoutbox"."shard_scope", "sentry_regionoutbox"."shard_identifier", "sentry_regionoutbox"."category", "sentry_regionoutbox"."object_identifier", "sentry_regionoutbox"."payload", "sentry_regionoutbox"."scheduled_from", "sentry_regionoutbox"."scheduled_for", "sentry_regionoutbox"."date_added" FROM "sentry_regionoutbox" WHERE ("sentry_regionoutbox"."id" <= 15 AND "sentry_regionoutbox"."shard_identifier" = 4554241598816256 AND "sentry_regionoutbox"."shard_scope" = 0) ORDER BY "sentry_regionoutbox"."id" ASC LIMIT 1 FOR UPDATE
-E RELEASE SAVEPOINT "s8377039552_x85"
-E SELECT 'end_role_override_28'
-E UPDATE "sentry_organizationmember" SET "user_id" = NULL WHERE "sentry_organizationmember"."id" = 7
-E ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-```
-
-Because we’re auditing SQL logs we can’t easily point to the specific line of code, but we can provide the query logs and where the problem query is.
-
-## Rollout
-
-Once we were confident that outboxes were being created correctly and state between the replica and source tables would reach consistency, we incrementally enabled outbox message creation and processing in production well before we began the process of splitting the database or application up. This allowed us to have confidence that the outbox system was behaving correctly, and establish baselines for performance, and message delivery.
-
-During the rollout period we were also able to gain experience adding and removing message types as our design and implementation became more self-evident.
-
-## Problems along the way
-
-With almost 1 billion outbox messages delivered, the system has been performing well, but hasn’t been without a few problems along the way.
-
-Initially we were using outboxes for delivering webhooks that are received from third-party integrations like GitHub. Delivering these webhooks to the relevant regions can incur a few seconds of latency. When we process an outbox message, we lock the row within a database transaction to prevent concurrent access. However, the delivery time on many webhooks exceeds our transaction timeout, resulting in slow and uneven message delivery. Instead of relaxing our transaction timeouts we chose to rebuild webhook delivery with different storage that didn’t require row locks.
-
-One challenge we haven’t solved yet is being able to detect and prevent outbox loops. It is entirely possible for the handler of an outbox message in a region to perform an action that creates outboxes in control silo that are then delivered to the region potentially creating an infinite loop. We had one such loop form during an incident. While we’re able to detect these loops and resolve them _after_ they happen it would be better to know that such events are impossible.
-
-Outside of those problems, transactional outboxes has proven to be a resilient and scalable design that we’re hoping to leverage more in the future.
diff --git a/src/content/blog/distributed-tracing-101-for-full-stack-developers.md b/src/content/blog/distributed-tracing-101-for-full-stack-developers.md
deleted file mode 100644
index bb81ec7f..00000000
--- a/src/content/blog/distributed-tracing-101-for-full-stack-developers.md
+++ /dev/null
@@ -1,204 +0,0 @@
----
-title: "Distributed Tracing 101 for Full Stack Developers"
-date: "2021-08-12"
-tags: ["performance", "web", "distributed tracing"]
-draft: false
-summary: "In today’s modern web stack it’s anything but. Full stack developers are expected to write JavaScript executing in the browser, interop with multiple database technologies, and deploy server side code on different server architectures (e.g. serverless). Without the right tools, understanding how a user interaction in the browser cascades into a 500 server error deep in your server stack is nigh-impossible. Enter: distributed tracing."
-images: []
-postLayout: PostLayout
-canonicalUrl: https://blog.sentry.io/2021/08/12/distributed-tracing-101-for-full-stack-developers/
-authors: ["benvinegar"]
----
-
-In the early days of the web, writing web applications was simple. Developers generated HTML on the server using a language like PHP, communicated with a single relational database like MySQL, and most interactivity was driven by static HTML form components. While debugging tools were primitive, understanding the execution flow of your code was straightforward.
-
-In today’s modern web stack it’s anything but. Full stack developers are expected to write JavaScript executing in the browser, interop with multiple database technologies, and deploy server side code on different server architectures (e.g. serverless). Without the right tools, understanding how a user interaction in the browser cascades into a 500 server error deep in your server stack is nigh-impossible. Enter: distributed tracing.
-
-
-
- _Me trying to explain a bottleneck in my web stack in 2021._
-
-
-_Distributed tracing_ is a monitoring technique that links the operations and requests occurring between multiple services. This allows developers to “trace” the path of an end-to-end request as it moves from one service to another, letting them pinpoint errors or performance bottlenecks in individual services that are negatively affecting the overall system.
-
-In this post, we’ll learn more about distributed tracing concepts, go over an end-to-end tracing example in code, and see how to use tracing metadata to add valuable context to your logging and monitoring tools. When we’re done, you’ll not only understand the fundamentals of distributed tracing, but how you can apply tracing techniques to be more effective in debugging your full stack web applications.
-
-But first, let’s go back to the beginning: what’s distributed tracing again?
-
-## Distributed tracing basics
-
-Distributed tracing is a method of recording the connected operations of multiple services. Typically, these operations are initiated by requests from one service to another, where a “request” could be an actual HTTP request, or work invoked through a task queue or some other asynchronous means.
-
-Traces are composed of two fundamental components:
-
-- A **span** describes an operation or “work” taking place on a service. Spans can describe broad operations – for example, the operation of a web server responding to an HTTP request – or as granular as a single invocation of a function.
-
-- A **trace** describes the end-to-end journey of one or more connected **spans**. A trace is considered to be a **distributed trace** if it connects spans (“work”) performed on multiple services.
-
-Let’s take a look at an example of a hypothetical distributed trace.
-
-
-
-The diagram above illustrates how a trace begins in one service – a React application running on the browser – and continues through a call to an API web server, and even further to a background task worker. The spans in this diagram are the work performed within each service, and each span can be “traced” back to the initial work kicked off by the browser application. Lastly, since these operations occur on different services, this trace is considered to be distributed.
-
-_Aside: Spans that describe broad operations (e.g. the full lifecycle of a web server responding to an HTTP request) are sometimes referred to as **transaction spans** or even just **transactions**._
-
-## Trace and span identifiers
-
-So far we’ve identified the components of a trace, but we haven’t described how those components are linked together.
-
-First, each trace is uniquely identified with a **trace identifier**. This is done by creating a unique randomly generated value (i.e. a UUID) in the **root span** – the initial operation that kicks off the entire trace. In our example above, the root span occurs in the Browser Application.
-
-Second, each span first needs to be uniquely identified. This is similarly done by creating a unique **span identifier** (or `span_id`) when the span begins its operation. This `span_id` creation should occur at every span (or operation) that takes place within a trace.
-
-
-
-Let’s revisit our hypothetical trace example. In the diagram above, you’ll notice that a trace identifier uniquely identifies the trace, and each span within that trace also possesses a unique span identifier.
-
-Generating `trace_id` and `span_id` isn’t enough however. To actually connect these services, your application must propagate what’s known as a **trace context** when making a request from one service to another.
-
-## Trace context
-
-The trace context is typically composed of just two values:
-
-- **Trace identifier** (or `trace_id`): the unique identifier that is generated in the root span intended to identify the entirety of the trace. This is the same trace identifier we introduced in the last section; it is propagated unchanged to every downstream service.
-- **Parent identifier** (or `parent_id`): the span_id of the “parent” span that spawned the current operation.
-
-The diagram below visualizes how a request kicked off in one service propagates the trace context to the next service downstream. You’ll notice that trace_id remains constant, while the parent_id changes between requests, pointing to the parent span that kicked off the latest operation.
-
-
-
-With these two values, for any given operation, it is possible to determine the originating (root) service, and to reconstruct all parent/ancestor services in order that led to the current operation.
-
-## A working example with code
-
-To understand this all better, let’s actually implement a bare-bones tracing implementation, using the example we’ve been returning to, wherein a browser application is the initiator of a series of distributed operations connected by a trace context.
-
-First, the browser application renders a form: for the purposes of this example, an “invite user” form. The form has a submit event handler, which fires when the form is submitted. Let’s consider this submit handler our root span, which means that when the handler is invoked, both a `trace_id` and `span_id` are generated.
-
-Next, some work is done to gather user-inputted values from the form, then finally a `fetch` request is made to our web server to the `/inviteUser` API endpoint. As part of this fetch request, the trace context is passed as two custom HTTP headers: `trace-id` and `parent-id` (which is the current span’s `span_id`).
-
-```js
-// browser app (JavaScript)
-import uuid from "uuid";
-
-const traceId = uuid.v4();
-const spanId = uuid.v4();
-
-console.log("Initiate inviteUser POST request", `traceId: ${traceId}`);
-
-fetch("/api/v1/inviteUser?email=" + encodeURIComponent(email), {
- method: "POST",
- headers: {
- "trace-id": traceId,
- "parent-id": spanId,
- },
-})
- .then((data) => {
- console.log("Success!");
- })
- .catch((err) => {
- console.log("Something bad happened", `traceId: ${traceId}`);
- });
-```
-
-_Note these are non-standard HTTP headers used for explanatory purposes. There is an active effort to standardize tracing HTTP headers as part of the W3C [traceparent](https://www.w3.org/TR/trace-context/) specification, which is still in the “Recommendation” phase._
-
-On the receiving end, the API web server handles the request and extracts the tracing metadata from the HTTP request. It then queues up a job to send an email to the user, and attaches the tracing context as part of a “meta” field in the job description. Last, it returns a response with a 200 status code indicating that the method was successful.
-
-Note that while the server returned a successful response, the actual “work” isn’t done until the background task worker picks up the newly queued job and actually delivers an email.
-
-At some point, the queue processor begins working on the queued email job. Again, the trace and parent identifiers are extracted, just as they were earlier in the web server.
-
-```js
-// API Web Server
-const Queue = require("bull");
-const emailQueue = new Queue("email");
-const uuid = require("uuid");
-
-app.post("/api/v1/inviteUser", (req, res) => {
- const spanId = uuid.v4(),
- traceId = req.headers["trace-id"],
- parentId = req.headers["parent-id"];
-
- console.log(
- "Adding job to email queue",
- `[traceId: ${traceId},`,
- `parentId: ${parentId},`,
- `spanId: ${spanId}]`,
- );
-
- emailQueue.add({
- title: "Welcome to our product",
- to: req.params.email,
- meta: {
- traceId: traceId,
-
- // the downstream span's parent_id is this span's span_id
- parentId: spanId,
- },
- });
-
- res.status(200).send("ok");
-});
-
-// Background Task Worker
-emailQueue.process((job, done) => {
- const spanId = uuid.v4();
- const { traceId, parentId } = job.data.meta;
-
- console.log(
- "Sending email",
- `[traceId: ${traceId},`,
- `parentId: ${parentId},`,
- `spanId: ${spanId}]`,
- );
-
- // actually send the email
- // ...
-
- done();
-});
-```
-
-_If you’re interested in running this example yourself, you can find the source code on [GitHub](https://github.com/getsentry/distributed-tracing-examples)._
-
-## Logging with distributed systems
-
-You’ll notice that at every stage of our example, a logging call is made using console.log that additionally emits the current **trace**, **span**, and **parent** identifiers. In a perfect synchronous world – one where each service could log to the same centralized logging tool – each of these logging statements would appear sequentially:
-
-
-
-If an exception or errant behavior occurred during the course of these operations, it would be relatively trivial to use these or additional logging statements to pinpoint a source. But the unfortunate reality is that these are distributed services, which means:
-
-- **Web servers typically handle many concurrent requests**. The web server may be performing work (and emitting logging statements) attributed to other requests.
-- **Network latency can cloud the order of operations**. Requests made from upstream services might not reach their destination in the same order they were fired.
-- **Background workers may have queued jobs**. Workers may have to first work through earlier queued jobs before reaching the exact job queued up in this trace.
-
-In a more realistic example, our logging calls might look something like this, which reflects multiple operations occurring concurrently:
-
-
-
-Without tracing metadata, understanding the topology of which action invoked which action would be impossible. But by emitting tracing meta information at every logging call, it’s possible to quickly filter on all logging calls within a trace by filtering on `traceId`, and to reconstruct the exact order by examining `spanId` and `parentId` relationships.
-
-This is the power of distributed tracing: by attaching metadata describing the current operation (span id), the parent operation that spawned it (parent id), and the trace identifier (trace id), we can augment logging and telemetry data to better understand the exact sequence of events occurring in your distributed services.
-
-## Tracing in the real world
-
-Over the course of this article, we have been working with a somewhat contrived example. In a real distributed tracing environment, you wouldn’t generate and pass all your span and tracing identifiers manually. Nor would you rely on `console.log` (or other logging) calls to emit your tracing metadata yourself. You would use proper tracing libraries to handle the instrumentation and emitting of tracing data for you.
-
-## OpenTelemetry
-
-[OpenTelemetry](https://opentelemetry.io/) is a collection of open source tools, APIs, and SDKs for instrumenting, generating, and exporting telemetry data from running software. It provides language-specific implementations for most popular programming languages, including both browser [JavaScript and Node.js](https://github.com/open-telemetry/opentelemetry-js).
-
-## Sentry
-
-[Sentry](https://sentry.io/) is an open source application monitoring product that helps you identify errors and performance bottlenecks in your code. It provides client libraries in every major programming language which instrument your software’s code to capture both error data and tracing telemetry.
-
-Sentry uses this telemetry in a number of ways. For example, Sentry’s [Performance Monitoring](https://sentry.io/for/performance/) feature set uses tracing data to generate waterfall diagrams that illustrate the end-to-end latency of your distributed services’ operations within a trace.
-
-
-
-Sentry additionally uses tracing metadata to augment its Error Monitoring capabilities to understand how an error triggered in one service (e.g. server backend) can propagate to an error in another service (e.g. frontend).
-
-You can learn more about [Sentry and distributed tracing here](https://sentry.io/features/distributed-tracing/).
diff --git a/src/content/blog/do-you-really-need-an-mcp-to-build-your-app.md b/src/content/blog/do-you-really-need-an-mcp-to-build-your-app.md
deleted file mode 100644
index 11f31854..00000000
--- a/src/content/blog/do-you-really-need-an-mcp-to-build-your-app.md
+++ /dev/null
@@ -1,188 +0,0 @@
----
-title: "Do you need an MCP to build your native app?"
-date: "2026-02-18"
-tags: ["ios", "ai", "mcp"]
-draft: false
-summary: "Do you need an MCP to build your native app? Surprisingly, modern agents succeed either way. The real difference is how much time, cost, and context you waste along the way."
-images: [../../assets/images/do-you-really-need-an-mcp-to-build-your-app/hero.png]
-canonicalUrl:
-authors: [cameroncooke]
----
-
-We recently [announced](https://blog.sentry.io/sentry-acquires-xcodebuildmcp) that Sentry acquired [XcodeBuildMCP](https://www.xcodebuildmcp.com/), the Model Context Protocol server I built to help AI agents navigate iOS development. One of the first questions we were asked was an uncomfortable one: is an MCP actually necessary? We're engineers building developer tools for engineers, so we did what felt natural and set out to answer it empirically.
-
-We built and ran an eval that measured three LLMs, against three approaches, each tasked with five different coding exercises totaling 1,350 trials to find out. **We expected XcodeBuildMCP to dominate, but it didn't.**
-
-**All three approaches we tested hit 99%+ success.** Modern models recover from errors well enough that finishing the task is basically guaranteed. What surprised us was where the real differences showed up: time, cost, and how each approach spends its context budget.
-
-
-
-## The Context Paradox
-
-MCP tools inject schemas, descriptions, and boilerplate into context before the agent does anything. That's useful for tool access, but context isn't free.
-
-Context is a budget. MCP tool schemas spend it up front; when agents don't have the right information, they spend it later on failed commands and retries. The question is which spend actually pays off.
-
-## The Experiment
-
-As mentioned above we tested three approaches across five tasks: a smoke test (build, install, launch) and 4 coding exercises (fix tests, implement caching, refactor an API, add a deep-link feature).
-
-1. **Shell (Unprimed):** No MCP tools, no guidance. The agent discovers scheme and simulator by running arbitrary commands.
-2. **Shell (Primed):** No MCP tools, but we gave the agent an `AGENTS.md` with the exact scheme, simulator destination, and project path.
-3. **MCP (Unprimed):** No `AGENTS.md`, but the agent has access to XcodeBuildMCP's full tool suite.
-
-## Methodology
-
-- 3 models (claude-opus, claude-sonnet, codex) × 5 tasks × 3 scenarios × 30 trials = 1,350 runs (9 baseline runs excluded from aggregates).
-- Success rate is per-run.
-- Time is median wall-clock seconds.
-- Tokens (avg) = uncached input + cached read + output (excludes cache writes).
-- Cost/Trial uses the cold-equivalent median (cached reads treated as uncached).
-- "Real tool errors" exclude XcodeBuildMCP session_defaults discovery, sibling-cascade errors, and build/test failures reported through tools; averaged per run.
-
-## Results
-
-| Metric | Shell (Unprimed) | Shell (Primed) | MCP (Unprimed) |
-| :--------------------------- | :---------------- | :-------------------- | :---------------- |
-| **Task Success Rate** | 99.78% (+0.22 pp) | **99.56% (baseline)** | 99.78% (+0.22 pp) |
-| **Median Time** | 185s (+50%) | **123s (baseline)** | 133s (+8%) |
-| **Tokens (avg)** | 400K (+17%) | **341K (baseline)** | 702K (+106%) |
-| **Cost/Trial (cold median)** | $1.12 (+14%) | **$0.98 (baseline)** | $2.30 (+135%) |
-| **Real Tool Errors (avg)** | 1.04 (+225%) | **0.32 (baseline)** | 0.56 (+75%) |
-
-_Cost/Trial uses cold-equivalent median cost (cached reads treated as uncached). In this run, cache read rates averaged ~91% for shell and ~96% for XcodeBuildMCP, so billed cost is substantially lower than cold. Percentages are relative to Shell (Primed); Task Success Rate uses percentage-point (pp) deltas. Most readers use subscription-based agents, so token counts may be more relevant than dollar figures._
-
-Success rates are nearly identical across all three. The story is in the time and cost columns.
-
-## A Markdown File Beat Everything on Cost
-
-As expected, the cheapest and fastest approach wasn't an MCP. It was a text file with four lines of build instructions. Primed shell finished 34% faster than unprimed shell, used 15% fewer tokens, and had 70% fewer real tool errors.
-
-Why? Minimal, targeted context. Just the build command: no schema overhead, no tool descriptions, no discovery cycles. The agent knows exactly what to run and runs it.
-
-For projects with stable build configurations, an `AGENTS.md` with your exact commands is the most direct path. Don't pay for discovery you don't need.
-
-## XcodeBuildMCP Cuts the Build Configuration Guesswork
-
-Building an iOS app with an agent requires getting three things right before a single line compiles: the project path, the scheme name, and a valid simulator destination. These aren't guessable. A project named "HackerNews" might have a scheme called "HackerNews", "Hacker News", or something else entirely. Simulators are identified by name, OS version, or UUID. Without guidance, the agent has to figure all of this out by running commands and reading error output.
-
-Without priming, shell agents spend early turns doing exactly that. A real unprimed run looks like this:
-
-```
-xcodebuild test -scheme "Hacker News" -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' ...
-xcodebuild: error: The project named "HackerNews" does not contain a scheme named "Hacker News".
-xcodebuild -list
-xcodebuild test -scheme HackerNews -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' ...
-xcodebuild: error: Unable to find a device matching the provided destination specifier.
-xcodebuild test -scheme HackerNews -destination 'platform=iOS Simulator,id=E3BD65D4-6AFC-48FA-9AF3-FE4D1EAE19DA' ...
-```
-
-Wrong scheme, wrong simulator, extra discovery, retries. Unprimed shell averaged **2.56 xcodebuild calls per trial** versus **1.25 for primed**.
-
-XcodeBuildMCP eliminates that guesswork. Its tools don't just return data; they include actionable hints that steer the agent toward the correct next call:
-
-```
-TOOL_CALL mcp__XcodeBuildMCP-Dev__list_schemes {}
-TOOL_RESULT ✅ Available schemes: HackerNews, ...
-Next Steps:
-1. Build the app: build_sim({ scheme: "HackerNews", simulatorName: "iPhone 16" })
-2. Show build settings: show_build_settings({ scheme: "HackerNews" })
-```
-
-Result: **28% faster median time** than unprimed shell, and p90 dropped about **20%**. The tool schema overhead is real, but it removes the failed-command cycles that bloat context with error output and retries. Context spent on the right thing is cheaper than context spent recovering from the wrong thing.
-
-## The Truncation Problem Is Worse Than It Looks
-
-A single `xcodebuild` call in our test project regularly exceeded agent truncation limits:
-
-```
-Output too large (1.2MB). Full output saved to: .../tool-results/toolu_01BVbVcVHRR7QLzTiqqcz6Gv.txt
-TOOL_CALL Bash {"command": "tail -100 /tmp/test_output.txt | grep -A 5 -B 5 \"Test Suite\\|passed\\|failed\""}
-```
-
-**49.6% of shell-unprimed** and **56.9% of shell-primed** trials hit truncation, with a median saved log of **~1.2MB**. When that happens, the agent typically runs `tail` to check the result, which means it may miss critical warnings that appeared earlier in the log. A build that "succeeded" could be emitting warnings about deprecated APIs or missing entitlements that the agent never sees.
-
-XcodeBuildMCP's `build_sim` tool filters output, returning only warnings, errors, and status. The median build result is **~2.1KB**, a **99.8% reduction** versus the median truncated shell log:
-
-```
-⚠️ Warning: ld: warning: search path '.../Frameworks/Reaper.xcframework' not found
-⚠️ Warning: warning: The CFBundleShortVersionString of an app extension ('1.0') must match that of its containing parent app ('3.10').
-✅ iOS Simulator Build build succeeded for scheme HackerNews.
-```
-
-XcodeBuildMCP uses 75% more tokens than unprimed shell overall, but the composition matters. Shell agents spend a large portion of their context budget on truncated multi-megabyte build logs and diagnostic retries. XcodeBuildMCP's tokens are nearly all structured, actionable data: warnings, errors, status messages, and next-step hints. The raw token count is higher; the noise is substantially lower.
-
-## Tool Errors: Almost Never the Problem
-
-688 of 1,350 runs (51%) hit at least one tool error. Almost none caused task failures. Models read the error, adjusted, and moved on.
-
-One nuance worth calling out: raw tool error counts are misleading for XcodeBuildMCP. The workflow intentionally surfaces a "missing session defaults" error the first time an agent discovers the correct setup call, and downstream failures often cascade from a single root error. The table below excludes those expected discovery errors and correctly-reported build/test failures:
-
-| Scenario | Raw Tool Errors (avg) | Real Tool Errors (avg) |
-| :--------------- | :-------------------: | :--------------------: |
-| Shell (Unprimed) | 1.04 | 1.04 |
-| Shell (Primed) | 0.32 | 0.32 |
-| MCP (Unprimed) | 1.20 | 0.56 |
-
-XcodeBuildMCP makes recovery easier with structured error messages that often suggest the fix directly. But modern models are good enough at recovery that errors alone are rarely the bottleneck.
-
-## Putting It All Together
-
-The eval answers the question it set out to answer: for simple, well-scoped coding tasks, all three approaches finish successfully. A primed `AGENTS.md` is the fastest and cheapest path; XcodeBuildMCP costs more up front but removes the discovery friction that bloat unprimed runs.
-
-What the eval couldn't measure is where XcodeBuildMCP's real ceiling is. The tasks were self-contained enough that the agent never needed to _see_ the running app. But XcodeBuildMCP isn't just a build wrapper, it gives agents a closed loop: capture a screenshot, inspect the view hierarchy, tap a button, set a breakpoint, read the console. That's a qualitatively different capability from anything a simple shell command can provide, and it's the one that matters for complex, multi-turn agentic workflows.
-
-The honest takeaway is that finishing a task and _knowing_ you finished it correctly are different things. For the former, a AGENTS.md file is enough. For the latter, verifying UI state, catching a regression in a live session, debugging a crash the agent just triggered, you need runtime access. That's the gap the eval didn't measure, and the gap XcodeBuildMCP is designed to close.
-
-## So What Should You Actually Do?
-
-**For routine builds on known projects** where you only need your agent to build and install the app, creating an `AGENTS.md` with your exact build parameters is sufficient:
-
-```markdown
-# Build Instructions
-
-## iOS Build
-
-- Project: `HackerNews.xcodeproj`
-- Scheme: `HackerNews`
-- Destination: `platform=iOS Simulator,name=iPhone 17 Pro`
-
-Run: `xcodebuild -project HackerNews.xcodeproj -scheme HackerNews -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build`
-```
-
-Fastest, cheapest, no overhead.
-
-**For a fully closed loop system:** Enable XcodeBuildMCP, your agent will be able to work autonomously and inspect and verify its own work as well as debug issues that arise:
-
-```json
-{
- "mcpServers": {
- "XcodeBuildMCP": {
- "command": "npx",
- "args": ["-y", "xcodebuildmcp@latest", "mcp"]
- }
- }
-}
-```
-
-## XcodeBuildMCP v2
-
-After we ran this eval we identified many areas of improvement for XcodeBuildMCP that we hoped would close the gap between the primed shell and XcodeBuildMCP. This actually turned out to be one of the most helpful uses of the eval, we had data and visibility we didn't have before. We made many improvements, reducing tool schema descriptions, only enabling simulator workflow by default, added stateful session support where XcodeBuildMCP remembers your project configuration, removing the need for the tools to include configuration parameters and reducing tool call overhead and much more.
-
-We tested a pre-release version of XcodeBuildMCP v2 using the same harness and tasks (15 trials per task per agent; n=225). Because v2 wasn't shipped at time of writing, **it's not included in the headline analysis above**, but it's useful as a directional check on whether we can reduce XcodeBuildMCP's context overhead without losing the discovery benefits.
-
-| Metric | Shell (Unprimed) | Shell (Primed) | MCP v1 (Unprimed) | MCP v2 (Unprimed) |
-| :--------------------------- | :---------------- | :-------------------- | :---------------- | :----------------- |
-| **Task Success Rate** | 99.78% (+0.22 pp) | **99.56% (baseline)** | 99.78% (+0.22 pp) | 100.00% (+0.44 pp) |
-| **Median Time** | 185s (+50%) | **123s (baseline)** | 133s (+8%) | 147s (+20%) |
-| **Tokens (avg)** | 400K (+17%) | **341K (baseline)** | 702K (+106%) | 453K (+33%) |
-| **Cost/Trial (cold median)** | $1.12 (+14%) | **$0.98 (baseline)** | $2.30 (+135%) | $1.27 (+30%) |
-| **Real Tool Errors (avg)** | 1.04 (+225%) | **0.32 (baseline)** | 0.56 (+75%) | 0.49 (+53%) |
-
-v2 cuts most of the token and cost overhead, trends toward fewer real tool errors, although in this run it is a bit slower in median wall-clock time. If these deltas hold at larger sample sizes, the core conclusion stays the same, but MCP becomes much more competitive on cost for discovery-heavy workflows.
-
-XcodeBuildMCP 2.x is now available with further optimizations to reduce context overhead and improve reliability. It introduces a CLI mode and Agent Skills that let your agent use XcodeBuildMCP without the upfront token cost mentioned above. For details, see the [changelog](https://github.com/getsentry/XcodeBuildMCP/blob/main/CHANGELOG.md).
-
----
-
-_The v1 dataset (1,350 runs) and evaluation harness are available on [GitHub](https://github.com/getsentry/xcodebuildmcp_eval); the v2 preview adds 225 runs in the same repo._
diff --git a/src/content/blog/enabling-out-of-the-box-performance-insights-in-the-unity-sdk.md b/src/content/blog/enabling-out-of-the-box-performance-insights-in-the-unity-sdk.md
deleted file mode 100644
index 06055cc7..00000000
--- a/src/content/blog/enabling-out-of-the-box-performance-insights-in-the-unity-sdk.md
+++ /dev/null
@@ -1,188 +0,0 @@
----
-title: "Enabling Out-of-the-Box Performance Insights in Unity Games with the Sentry SDK"
-date: "2024-11-04"
-tags: ["unity", "sdk", "c#"]
-draft: false
-summary: "Learn how we built the autoinstrumentation in the Unity SDK via IL Weaving"
-images: [../../assets/images/enabling-out-of-the-box-performance-insights-in-the-unity-sdk/hero.jpg]
-postLayout: PostLayout
-canonicalUrl:
-authors: [stefanjandl]
----
-
-## Introduction: From Crash Reporting to Performance Insights
-
-Our Unity SDK was super complete from the crash reporting point of view. It had support for line numbers in C# exceptions on IL2CPP (in release mode!), captured native crashes on Windows, macOS, Linux Android and iOS, context set via C# would show up on any type of event, including minidumps, debug symbols are magically uploaded when you build the game with the editor. And more. We were confident we had the best crash reporting solution out there. Now we were looking towards offering some out-of-the-box insights into the game’s performance. Right out of the gate we hit the first question: What would auto-instrumentation for Unity games look like?
-
-## Adapting Sentry's Performance UX for Unity
-
-Sentry had built UX for visualization of span trees and the instrumentation for mobile and web is based around screen rendering. We wanted to take those concepts and apply them to Unity. As a result we limited the instrumentation to the game’s startup procedure and scene loading. Every game starts at some point. And every game, no matter how big or small, loads a scene. We might not be able to give insights into the entire game but we can show every developer right after installing the package, what Sentry could offer.
-
-Our ideal scenario would be something that would work out-of-the-box with no to minimal setup from the user. As a sneak-peek and to show off what we got working without you having to read the whole thing before you get as excited as we are: This is what the Unity SDK‘s auto-instrumentation offers OOTB right now. Without a single line of code. For all Unity games.
-
-
-
-## Introducing Sentry SDK for Unity: A Multi-Platform Tool
-
-Unity games run on basically all platforms. To provide support for that, the Sentry SDK for Unity became an SDK of SDKs. It ships and integrates via P/Invoke (FFI) with whatever SDK is native for the targeted platform. Running on iOS? Not a problem, we’ll bring the [Sentry SDK for Apple](https://github.com/getsentry/sentry-cocoa) to have you covered! Same for [Android](https://github.com/getsentry/sentry-java), WebGL, and all the desktops!
-
-
-
-After all, this is how we achieved the native crash capturing support. What those SDKs also have in common, other than powering the Unity SDK, they all provide some form of auto instrumentation.
-Unfortunately, this has limited use. A key factor in Unity’s success is its platform abstraction. Developers are free from worrying about platform specifics and that allows them to focus solely on Unity internals. To enable this, Unity games are typically embedded within a super thin launcher. As a result, concepts like navigation events and UI activities from the underlying platform are generally unfamiliar to them. For instrumentation to be truly helpful and actionable, the SDK would need to operate directly within Unity.
-
-## Understanding the Unity Lifecycle: Finding Key Points for Instrumentation
-
-The game works in a super tight loop, typically updating anywhere from 30 to 60 times per second but the sky is the limit. Creating a span to measure every single tick is not feasible. We needed to look at some overarching actions like some set of logical operations we would want to capture.
-
-
-
-### The Challenge of Defining Transactions and Spans
-
-To measure how long something takes, Sentry has two working concepts: Transactions and Spans. Transactions are single [instances of an activity or a service](https://docs.sentry.io/product/performance/transaction-summary/#what-is-a-transaction), like loading of a page or some async task. Spans are individual measurements that are nested within a transaction. Conceptionally, we're trying to find places to start and stop a big stopwatch for bigger, and very specific actions that we want to measure. And then we are looking for sub-tasks within that action that we could capture with smaller stopwatches. But how does a transaction fit within the the frame of a game? What instance of a service, that is already built into the engine, could a transaction represent?
-
-For all its features Unity is still a blank canvas for you to create any kind of game. That means there are, other than the general lifecycle, not very many fixed points that the SDK could hook into to start and stop a span. There are a whole bunch of one-time events like button clicks but how would the SDK hook into whatever happens behind the button click? How would the SDK know when to finish the span?
-
-## Universal Events in Unity: Startup and Scene Loading
-
-All games need to startup and the startup procedure is the same for all Unity games and consists of loading of systems, the splash-screen (if applicable) and the loading of the initial scene. Scene loading in general is another great fixed point. Everything within Unity exists within the context of a scene. Some games load them additionally, some games swap them, some games only ever have one. But at least that one gets loaded during startup.
-
-With this we had our transaction hooks. We know when the startup is starting and finished. And we can hook into the scene manager to time scene load and scene load finish events.
-
-## Adding Granularity: Populating Transactions with Spans
-
-Now that we have our overarching operation that we’re trying to time we’re now looking for smaller actions that happen within. Looking towards Unity’s lifecycle helps us out once more. The initialization happens for every GameObject during its creation or, if it is an initial part of the scene, during the scene’s loading. For all GameObjects the one method that gets invoked is the `Awake` call. And that’s the user’s code, which is exaclty what we would like to instrument. That's the code the user has control over and where we want to highlight performance opportunities or bottlenecks. But how would the SDK instrument non-SDK code without asking to user to do it for us?
-
-## IL Weaving - The art of... _insert pun_
-
-When working on your Unity game you’re typically writing your code in C#. And no matter what the end-result, even tho it later gets compiled to platform native code via IL2CPP, at some point in the build process that C# code gets compiled to [Intermediate Language](https://learn.microsoft.com/en-us/dotnet/standard/managed-code) (IL). Even tho Unity might transpile the IL to C++ later on, that IL is still there, somewhere. We managed to hook the SDK into the the build pipeline to modify the generated `Assembly-CSharp.dll`.
-
-Let’s say we have a very simple `MonoBehaviour` for demonstration purposes:
-
-```csharp
-using UnityEngine;
-
-public class BlogMaterial : MonoBehaviour
-{
- private void Awake()
- {
- Debug.Log("Hello World!");
- }
-}
-```
-
-This MonoBehaviour compiles to the following IL:
-
-```csharp
-.class public auto ansi beforefieldinit BlogMaterial
- extends [UnityEngine.CoreModule]UnityEngine.MonoBehaviour
-{
- // Methods
- .method private hidebysig
- instance void Awake () cil managed
- {
- // Method begins at RVA 0x2160
- // Header size: 1
- // Code size: 11 (0xb)
- .maxstack 8
-
- // Debug.Log((object)"Hello World!");
- IL_0000: ldstr "Hello World!"
- IL_0005: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)
- // }
- IL_000a: ret
- } // end of method BlogMaterial::Awake
-} // end of class BlogMaterial
-
-```
-
-## Writing code that writes code with Cecil
-
-We want to wrap whatever is going on inside the `Awake` with a span. For this we created some helpers that are accessible from anywhere inside the user’s code.
-
-```csharp
-///
-/// A MonoBehaviour used to provide access to helper methods used during Performance Auto Instrumentation
-///
-public partial class SentryMonoBehaviour
-{
- public void StartAwakeSpan(MonoBehaviour monoBehaviour) =>
- SentrySdk.GetSpan()?.StartChild("awake", $"{monoBehaviour.gameObject.name}.{monoBehaviour.GetType().Name}");
-
- public void FinishAwakeSpan() => SentrySdk.GetSpan()?.Finish(SpanStatus.Ok);
-}
-```
-
-Initially, we did this change manually and took a look at the resulting IL.
-
-```csharp
-private void Awake()
-{
- SentryMonoBehaviour.Instance.StartAwakeSpan(this);
-
- Debug.Log("Hello World!");
-
- SentryMonoBehaviour.Instance.FinishAwakeSpan();
-}
-```
-
-```csharp
- // Methods
- .method private hidebysig
- instance void Awake () cil managed
- {
- // Method begins at RVA 0x22f7
- // Header size: 1
- // Code size: 32 (0x20)
- .maxstack 8
-
- // SentryMonoBehaviour.Instance.StartAwakeSpan(this);
- IL_0000: call class [Sentry.Unity]Sentry.Unity.SentryMonoBehaviour [Sentry.Unity]Sentry.Unity.SentryMonoBehaviour::get_Instance()
- IL_0005: ldarg.0
- IL_0006: callvirt instance void [Sentry.Unity]Sentry.Unity.SentryMonoBehaviour::StartAwakeSpan(class [UnityEngine]UnityEngine.MonoBehaviour)
- // Debug.Log("Hello World!");
- IL_000b: ldstr "Hello World!"
- IL_0010: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)
- // SentryMonoBehaviour.Instance.FinishAwakeSpan();
- IL_0015: call class [Sentry.Unity]Sentry.Unity.SentryMonoBehaviour [Sentry.Unity]Sentry.Unity.SentryMonoBehaviour::get_Instance()
- IL_001a: call instance void [Sentry.Unity]Sentry.Unity.SentryMonoBehaviour::FinishAwakeSpan()
- // }
- IL_001f: ret
- } // end of method BlogMaterial::Awake
-```
-
-But the `.dll` is not just a text file. So how do we modify this? Luckily, there are libraries like [Cecil](https://github.com/jbevain/cecil) around that we can build on and that do the heavy lifting for us. Cecil basically turns this into something akin to painting by numbers:
-
-1. Compile your code to create the “baseline”
-2. Modify the source code to your desired end-result
-3. Let the compiler do what it does best - translate your C# code into IL
-4. Inspect the difference between the baseline and the end-result
-5. Use Cecil to recreate the change
-
-The code that modifies the `Awake` and adds the `StartSpan` functionality is three lines:
-
-```csharp
-// Adding in reverse order because we're inserting *before* the 0ths element
-processor.InsertBefore(method.Body.Instructions[0], processor.Create(OpCodes.Callvirt, startAwakeSpanMethod));
-processor.InsertBefore(method.Body.Instructions[0], processor.Create(OpCodes.Ldarg_0));
-processor.InsertBefore(method.Body.Instructions[0], processor.Create(OpCodes.Call, getInstanceMethod));
-```
-
-You can inspect the whole setup of reading, modifying and writing the IL [here](https://github.com/getsentry/sentry-unity/blob/c646ffcdb7a751663d21f41f88d1f36dfc86361f/src/Sentry.Unity.Editor/AutoInstrumentation/SentryPerformanceAutoInstrumentation.cs).
-
-And the result is this Trace View for every Unity game out-of-the-box, without the user having to write a single line of code.
-
-
-
-## What do we have now?
-
-With this IL weaving setup, we accomplished two goals:
-
-- Immediate, visible performance value: Developers see auto-instrumented performance insights without adding extra code.
-- A foundation for future expansion: We proved it’s viable to inject custom SDK functionality that wraps user code, enabling future opportunities for auto-instrumentation.
-
-## Where to go from here
-
-This setup opens the door for even more instrumentation possibilities. For instance, [UnityWebRequests](https://github.com/getsentry/sentry-unity/issues/737) could be instrumented automatically, or we could explore adding spans to button clicks by timing actions around them.
-
-Stay tuned as we continue to expand what’s possible with Sentry’s Unity SDK!
diff --git a/src/content/blog/formatting-sql-on-the-frontend.md b/src/content/blog/formatting-sql-on-the-frontend.md
deleted file mode 100644
index 34f66f34..00000000
--- a/src/content/blog/formatting-sql-on-the-frontend.md
+++ /dev/null
@@ -1,380 +0,0 @@
----
-title: "Formatting SQL in the Browser Using PEG"
-date: "2025-04-08"
-tags: ["sql", "react", "web", "javascript"]
-draft: false
-summary: "Writing a rudimentary SQL parser and formatter in JavaScript that handles Sentry's need to format invalid SQL and output into JSX."
-images: ["../../assets/images/formatting-sql-on-the-frontend/header.jpg"]
-postLayout: PostLayout
-canonicalUrl:
-authors: ["georgegritsouk"]
----
-
-**UPDATE:** Our SQLish formatter is now available as a [standalone package](https://github.com/getsentry/sqlish)!
-
----
-
-Sentry's Performance team (a team I'm on, the team that works on features like [Insights](https://docs.sentry.io/product/insights/), [Dashboards](https://docs.sentry.io/product/dashboards/), [Explore](https://docs.sentry.io/product/explore/), and others) spent a big chunk of 2023 working on a database monitoring feature called ["Queries"](https://docs.sentry.io/product/insights/backend/queries/). "Queries" is a UI that shows information about SQL queries and their performance. SQL code is a central focus, so it became important to have good SQL formatting. None of the existing SQL formatters fit our needs, so we wrote our own! Our formatter has a few interesting features, so in this post I'll explain what those features are, why they are interesting, and how we wrote our implementation.
-
-## Interesting Feature 1: Support for Invalid SQL
-
-To format SQL, first you have to parse SQL. Many off-the-shelf SQL parsers are _validating_ which means they will fail on SQL that isn't valid. At Sentry, a _lot_ of the SQL we display is _very_ invalid. Here are a few examples of "SQL" we need to support:
-
-```sql
-SELECT * FROM users WHERE users.id = %s; /* Django ORM placeholders are not valid SQL! */
-SELECT * FROM users WHERE users.ip_address = *; /* IP addresses are PII, and Relay strips them out */
-SELECT * FROM users WHERE users.id IN (...) /* Long `IN` condition stripped out to reduce cardinality */
-SELECT * FROM users WHER... /* Query was too long, we truncated the end */
-```
-
-Our formatter successfully formats all kinds of invalid SQL-looking strings. For example, the string `'SELECT * FROM (SELECT * FROM use..'` is not valid SQL, but is formatted as:
-
-```sql
-SELECT *
-FROM (
- SELECT *
- FROM use..
-```
-
-**Aside:** You can learn more about Relay in our [documentation](https://develop.sentry.dev/ingestion/relay/) and if you're curious about SQL parameterization, we have [some documentation](https://docs.sentry.io/product/insights/backend/queries/#query-parameterization) about that, too.
-
-## Interesting Feature 2: Support for JSX Output
-
-I wanted our formatter to support multiple output types. I had an idea about _gentle_ formatting done via bolding and italics, and I had some ambitions about _interactive_ formatting (e.g., hovering on a table name in a query would show information about that table). Only some of those ambitions materialized, but to make them possible at all we needed a formatter that can output JSX nodes that we can attach styling to.
-
-Our formatter supports two output types. The first output type is plain string, with spacing and indentation. This is well suited for displaying full queries, with line breaks, indentation, and syntax highlighting. Here's an example of the UI it enables:
-
-
-
-The second output type is an array of JSX elements, with `` tags wrapping the important tokens. This is suitable for showing long, scannable lists of queries. Each query is shown on a single line, with just a hint of highlighting for the important tokens for readability. Here's an example:
-
-
-
-Fully highlighted strings wouldn't make sense here. They'd take up multiple lines, they'be be overwhelming, and we want links to be _blue_, to indicate that they're links.
-
-## Parse, Format
-
-Let's get into how this works, and why it does what it does. First, here's an example of how to use the formatter:
-
-```jsx
-const formatter = new SQLishFormatter();
-const output = formatter.toString("SELECT hello FROM users ORDER BY name DESC LIMIT 1;");
-console.log(output);
-
-// SELECT hello
-// FROM users
-// ORDER BY name DESC
-// LIMIT 1;
-```
-
-Under-the-hood, there are two steps. The first is to **parse** the input string using [a PEG parser](https://en.wikipedia.org/wiki/Parsing_expression_grammar) (more on this soon) into a [parse tree](https://en.wikipedia.org/wiki/Parse_tree) (more on this soon). The second is to take the parse tree and either **format** it as a string, or **format** it as JSX.
-
-## Parsing
-
-### A Gentle Introduction to Parsing
-
-In order to transform a raw string of SQL to a rich output format, first one must parse the string. Parsing is usually done in two steps. The first step is to lexically analyze the string and split it into small chunks called "tokens". The second is to take those tokens and construct a tree structure that describes the code in a way it could be transformed to bytecode and executed. This tree is called a ["parse tree"](https://en.wikipedia.org/wiki/Parse_tree).
-
-For example, consider the query:
-
-```sql
-SELECT hello
-FROM users
-LIMIT 1;
-```
-
-Tokenizing this would produce this array of strings:
-
-```javascript
-["SELECT", " ", "hello", "\n", "FROM", " ", "users", "\n", "LIMIT", " ", "1", ";"];
-```
-
-The key thing to notice is that it's an array that contains all the characters from the input string.
-
-Transforming it into a parse tree would create a structure that looks something like:
-
-```json
-{
- "type": "program",
- "statements": [
- {
- "type": "select_stmt",
- "clauses": [
- {
- "type": "select_clause",
- "selectKw": {
- "type": "keyword",
- "text": "SELECT",
- "name": "SELECT",
- "range": [
- 0,
- 6
- ]
- },
- "options": [],
- "columns": {
- "type": "list_expr",
- "items": [
- {
- "type": "column_ref",
- "column": {
- "type": "identifier",
- "text": "hello",
- "range": [
- 7,
- 12
- ]
- },
-```
-
-The key thing to notice is that it's a deeply nested tree that accounts for the intricacies of SQL. Each token is given semantic structural meaning (is it a command? Is it a parameter?), its content (e.g. `"SELECT"`), its position in the original string (e.g., `7, 12`) and other important metadata.
-
-One way to accomplish this is to write a tokenizer that would split the string, and also write a parser that would create a tree structure from the tokens.
-
-Another way to do this is to write a _grammar_. A grammar is a formal definition of a language, in a special syntax. The neat thing about grammars is that _some_ grammars can be _automatically converted to a parser_! PEG is one such grammar. PEG parsers have some constraints and some known benefits that were acceptable to us, so that's the route we took. Plus, we already use PEG in some other places in the app.
-
-**Aside:** The tree above was made using https://astexplorer.net, a really great AST exploration tool.
-
-### Constructing a Grammar
-
-If you're wondering what a full SQL grammar looks like, you can find one [on the internet](https://github.com/alsotang/sql.pegjs/blob/master/lib/sql.pegjs). It's a lot. We do not want a full SQL grammar. What we want is a grammar that's aware of the _basics_ of the language (keywords, operations, parameters, syntax markers). Our grammar cannot be aware of nesting, because a truncated query cannot be parsed, since the nesting might not be closed. Our grammar cannot be aware of hyper-specific syntax like casting, since it's not supported in all SQL dialects. Our grammar must support _very invalid_ characters like `*` in strange places. Therefore, our grammar (luckily for me) needs to be very simple.
-
-Here's an example of a very simple grammar for SQL, even simpler than the one we're using in production:
-
-```peg
-Expression
- = tokens:Token*
-
-Token
- = Whitespace / Keyword / Unknown
-
-Keyword
- = Keyword:("SELECT"i / "FROM"i) {
- return { type: 'K', content: Keyword.toUpperCase() }
-}
-
-Whitespace
- = Whitespace:[\n\t\r ]+ { return { type: 'W', content: Whitespace.join('') } }
-
-Unknown
- = GenericToken:[a-zA-Z0-9"'*;]+ { return { type: 'U', content: GenericToken.join('') } }
-```
-
-The specific syntax might look foreign to you, but even at a glance you can see the basics:
-
-- the grammar consists of a flat list of tokens, rather than a recursive definition
-- each token can be whitespace, a "keyword", or something unknown
-- whitespace is one of several known whitespace characters
-- a "keyword" is one of a few known SQL keywords
-- "unknown" is a catch-all for other random characters
-- the `.toUpperCase()` and `.join('')` give you a hint of what is returned when parsing runs
-
-If you're curious what the resulting parser looks like, here's a snippet:
-
-```js
-function peg$parse(input, options) {
- options = options !== undefined ? options : {};
-
- var peg$FAILED = {};
- var peg$source = options.grammarSource;
-
- var peg$startRuleFunctions = { Expression: peg$parseExpression };
- var peg$startRuleFunction = peg$parseExpression;
-
- var peg$c0 = "select";
- var peg$c1 = "from";
-
- var peg$r0 = /^[\n\t\r ]/;
- var peg$r1 = /^[a-zA-Z0-9"'*;]/;
-
- var peg$e0 = peg$literalExpectation("SELECT", true);
- var peg$e1 = peg$literalExpectation("FROM", true);
- var peg$e2 = peg$classExpectation(["\n", "\t", "\r", " "], false, false);
- var peg$e3 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"], "\"", "'", "*", ";"], false, false);
-...
-```
-
-**Aside:** We run [Peggy](https://peggyjs.org) using Webpack. It compiles our `.pegjs` files to `.js` parser files.
-
-You can see some some familiar tokens in the parser code, concepts we defined in the grammar.
-
-This grammar knows about `SELECT` and `FROM` keywords, about whitespace, and a few other character. Here's the tree it spits out for the SQL string `SELECT * FRO`:
-
-```js
-[
- {
- type: "K",
- content: "SELECT",
- },
- {
- type: "W",
- content: " ",
- },
- {
- type: "U",
- content: "*",
- },
- {
- type: "W",
- content: " ",
- },
- {
- type: "U",
- content: "FRO",
- },
-];
-```
-
-You'll notice a few things:
-
-1. The token `SELECT` is recognized as type `"K"` (keyword)
-2. The token `*` is recognized as type `"U"` (unknown) but doesn't cause the parser to fail
-3. The output is a flat array with no nesting
-
-If you're thinking "this is just a tokenizer with extra steps" you're not wrong. It's not much of a tree. Maybe it's a bamboo stalk. I don't know, I'm not an arborist! In any case, so far so good. This is actually enough for simple queries. We could iterate this flat array, create `` elements, or do whatever we want.
-
-### Improving a Grammar
-
-The difference between this grammar and what we're using in production is not huge:
-
-- More keywords. Our full grammar supports about 30 common ones
-- Parentheses. In order to know where to indent and add newlines, we want to know where parentheses are
-- More special characters. Supporting all known ASCII characters (and even emoji, and other Unicode craziness) is important, so we need to extend what "unknown" is
-- Complicated operations like `JOIN`, so we can indent and highlight those
-- Special Sentry characters like `..`
-
-I won't go into full detail, you can see the grammar [on GitHub](https://github.com/getsentry/sentry/blob/master/static/app/utils/sqlish/sqlish.pegjs#L4), but I'll give you two highlights.
-
-1. The `CollapsedColumns` token is a special string that denotes a long list of parameters, inserted by Relay. This is a Sentry-aware formatter, so it handles many Sentry-isms in the data:
-
-```peg
-CollapsedColumns
- = ".." { return { type: 'CollapsedColumns', content: '..' } }
-```
-
-2. `GenericToken` is a catch-all for pretty much all known characters in the entire Unicode BMP _including_ surrogate pairs and unassigned code points. Talking about Unicode is so far outside the scope of this post I don't even want to touch it:
-
-```peg
-GenericToken
- = GenericToken:[a-zA-Z0-9\u00A0-\uFFFF"'`_\-.=><:,*;!\[\]?$%|/\\@#&~^+{}]+ { return { type: 'GenericToken', content: GenericToken.join('') } }
-```
-
-Combining enough of these expressions makes it possible to parse just about anything.
-
-## Formatting as a String
-
-String formatting needs to do four main things.
-
-1. Create newlines for important keywords
-2. Increase indentation for some parentheses
-3. Wrap the code at a reasonable length
-4. Syntax highlighting
-
-Turns out, it's pretty simple to do those things with simple heuristics! By checking the current token, the preceding token, the current indentation level, and the current nesting level, we can handle very sophisticated queries.
-
-The pseudocode for formatting is pretty simple. Go token-by-token. An open parenthesis increases the indentation level. A meaningful keyword (e.g., `SELECT`) creates a newline. A closed parenthesis decreases the indentation level. After initial formatting, go through the formatted lines, and wrap them if needed. There are many edge cases to cover, but that's the gist! You can see the full code [on GitHub](https://github.com/getsentry/sentry/blob/master/static/app/utils/sqlish/formatters/string.ts).
-
-The last piece is syntax highlighting. We have enough information to do this ourselves (we know which strings are important keywords), but there's no need. [Prism](https://prismjs.com) is a very popular open-source non-validating syntax highlighter that suits our needs just fine. That's it!
-
-A reminder of what the output looks like:
-
-
-
-## Formatting as JSX
-
-JSX formatting is even simpler. It's _very_ simple. Go token-by-token. If the token is known to be a keyword, return it wrapped in ``. If it's whitespace, return a single space. If it's something else, return it wrapped in a ``. That's the whole formatter. Then we can use CSS to style the output however we like, add click handlers, and so on. Here's the same screenshot as above as a reminder of the output format:
-
-
-
-You can see the full code, again, on [GitHub](https://github.com/getsentry/sentry/blob/master/static/app/utils/sqlish/formatters/simpleMarkup.ts)
-
-## Telemetry
-
-Everyone at Sentry is low-key obsessed with gathering telemetry. Me too! Before I could ship this to everyone, I had to answer two important questions. The first is, how often are we failing to parse a query? The second is, how fast does this formatter run?
-
-### How Often
-
-Remember the `GenericToken` piece of the grammar? That was hard-fought. I _could_ have allowed literally every character right off the bat, but I wanted to learn. I started slowly rolling out the new formatter, and would throw an exception every time the parser failed. The parser degrades very gracefully to an unformatted but still syntax-highlighted string. Every time I saw an exception, I would figure out what syntax I missed, and add a test case for it.
-
-```js
-try {
- tokens = this.parser.parse(sql);
-} catch (error) {
- Sentry.withScope((scope) => {
- scope.setFingerprint(["sqlish-parse-error"]);
- // Get the last 100 characters of the error message
- scope.setExtra("message", error.message?.slice(-100));
- scope.setExtra("found", error.found);
- Sentry.captureException(error);
- });
- // If we fail to parse the SQL, return the original string
- return sql;
-}
-```
-
-The result is a list of specific, descriptive test cases that explain why certain characters are part of the set, and what they mean:
-
-```js
-'AND created >= :c1', // PHP-Style I
-'LIMIT $2', // PHP-style II
-'created >= %s', // Python-style
-'created >= $1', // Rails-style
-'@@ to_tsquery', // Postgres full-text search
-'FROM temp{%s}', // Relay integer stripping
-'+ %s as count', // Arithmetic I
-'- %s as count', // Arithmetic II
-...
-```
-
-I kept going until `GenericToken` had everything we saw in the wild, and the issue stopped appearing.
-
-### How Fast
-
-The second question is, is this parser fast enough to run in production? I manually [instrumented](https://docs.sentry.io/platforms/javascript/tracing/instrumentation/) Sentry spans for the formatter, so I could track how long formatting actually takes on real users' computers. Benchmarks are nice, but contact with reality is brutal and I'd rather learn from reality right away.
-
-```js
-const sentrySpan = Sentry.startInactiveSpan({
- op: 'function',
- name: 'SQLishFormatter.toFormat',
- attributes: {
- format,
- },
- onlyIfParent: true,
-});
-
-...
-
-sentrySpan?.end();
-```
-
-The first set of measurements was:
-
-- p75 is 0.2ms (very fast!)
-- p95 is 6.0s (hmm)
-
-Not _amazing_ results at the 95th percentile, but this was before any optimizations (I added some later) and the browser environment is _so_ volatile, some wild outliers will always show up. I was comfortable with this as a starting point, especially since spot-checking long queries looked good, and because parsing only happens one per page load, and our main source of user-perceived slowness is data loading anyway.
-
-I checked the data today, and here's what I saw:
-
-
-
-Not bad!
-
-**Aside:** If words like "p95" don't mean anything to you, [Wikipedia has a thorough explainer on percentiles](https://en.wikipedia.org/wiki/Percentile).
-
-## Conclusion
-
-If you're using Sentry's "Queries" feature, and you're looking at formatted queries, you're looking at the output of this formatter. I read online somewhere that once you understand parsers, you start to see everything as a parsing problem. After this project, I'm starting to agree.
-
-## Resources
-
-Here are some resources I used when I was working on this project:
-
-- [The Super Tiny Compiler](https://glitch.com/edit/#!/the-super-tiny-compiler) is a _beautiful_ project on Glitch. It's a fully annotated and deeply explained simple compiler
-- [Wikipedia's entry on PEG](https://en.wikipedia.org/wiki/Parsing_expression_grammar) was an obvious first choice, and it even has resources on esoteric topics like [removing left recursion](https://en.wikipedia.org/wiki/Left_recursion#Removing_left_recursion)
-- [A Text Pattern-Matching Tool based on Parsing Expression Grammars](http://www.inf.puc-rio.br/~roberto/docs/peg.pdf) is a really interesting (but dense) paper. I must have read something in it that I liked, but now it was so long ago I don't remember what it was
-- Guido van Rossum has [a series about PEG on Medium](https://medium.com/@gvanrossum_83706/peg-parsers-7ed72462f97c) that was paywalled at the time, but seemed promising
-- [This medium post about PEG and Lua/SQL](https://medium.com/@brynne8/lpeg-and-peg-practices-b3d0fc00457e) was interesting and helpful, though I ended up going in a different direction
-- [pegedit](http://pegedit.cspotrun.org) was recommended online as a resource for tinkering with PEG, but I mostly used Peggy's own [playground](https://peggyjs.org/online.html)
-
-I also found a bunch of lectures on PEG grammars online from—I think—Princeton, but now I can't find them. Sorry!
diff --git a/src/content/blog/from-monkey-patching-to-tracing-channels.md b/src/content/blog/from-monkey-patching-to-tracing-channels.md
deleted file mode 100644
index cc0a1cb4..00000000
--- a/src/content/blog/from-monkey-patching-to-tracing-channels.md
+++ /dev/null
@@ -1,178 +0,0 @@
----
-title: "There Are Better Ways Than Monkey-Patching: A Path to Node.js Observability With Tracing Channels"
-date: "2026-04-13"
-tags: ["javascript", "sdk", "opentelemetry"]
-draft: false
-summary: "What if libraries were active participants in their own observability and emit telemetry data themselves? Here's how Node's diagnostics API can make that a reality."
-images: ["../../assets/images/from-monkey-patching-to-tracing-channels/hero.png"]
-postLayout: PostLayout
-canonicalUrl: https://blog.sentry.io/observability-with-tracing-channels/
-authors: ["sigridhuemer"]
----
-
-Almost every production application uses a number of different tools and libraries. Be it a library to communicate with
-a database, a cache, or frameworks like Nest.js or Nitro. To be able to observe what’s going on in production,
-application developers reach out for Application Performance Monitoring (APM) tools like Sentry. But there’s an inherent
-problem: the performance data that APM tools need is most often not coming natively from the library itself. Getting
-this data is delegated to APM tools like Sentry or OpenTelemetry, which instrument crucial functionality of a library on
-their behalf.
-
-## What Is Instrumentation?
-
-The most fundamental requirement to make an application observable is the ability to instrument each of its components
-and used libraries. Instrumentation is the process of adding code to a program to monitor and analyze its internal
-operations and generate diagnostic data. It’s exactly what the Sentry SDKs and OpenTelemetry instrumentation are doing
-under the hood.
-
-Consider a typical HTTP client library. Application developers want to know when a request starts and completes, along
-with some metadata like URL, status code and headers. Today, libraries handle this inconsistently: some provide custom
-hooks like `emitter.on('request', ...)`, others offer vendor-specific middleware to intercept requests. In these cases,
-Sentry and OpenTelemetry can write plug-ins that emit observability data.
-
-This works, but it puts the burden on the library or framework (e.g. Nuxt) to consciously design an instrumentation API
-and identify the right places to expose it. Hooks and interceptors allow injecting observability code at the correct
-spots, but APM maintainers are entirely dependent on library authors to keep those APIs stable over time. On top of
-that, there is no shared convention (each library exposes different hook shapes and different metadata) so APM
-maintainers must write and maintain very different plugins for each library.
-
-## How server-side JavaScript is instrumented
-
-The traditional approach to JavaScript instrumentation is “monkey-patching”. That’s modifying library code at runtime so
-that library functions not only do their original job, but also emit observability data. This is only possible in
-CommonJS (CJS), where modules are mutable and synchronously loaded.
-
-However, the ecosystem is shifting. As server-side JavaScript moves further toward ES Modules (ESM), this approach
-breaks down. ES modules are immutable and loaded asynchronously, which means you simply can't patch imports at runtime
-the same way anymore. For further information: the [ESM Observability Instrumentation Guide](https://github.com/getsentry/esm-observability-guide) covers this topic in greater detail.
-
-The current workaround (and a way to “patch” imports) is using Module Customization Hooks paired with the `--import`
-flag. A popular hook is `import-in-the-middle/hook.mjs`. It works, but it's brittle, complex, and feels like what it is:
-a workaround.
-
-Both monkey-patching in CJS and Module Customization Hooks in ESM share the same fundamental flaw: they apply
-instrumentation “_from the outside”._ The library itself is passive. The question worth asking is: **what if libraries
-were active participants in their own observability and emit telemetry data themselves?** This would be possible through
-diagnostics APIs like Tracing Channels.
-
-## Libraries Should Emit Their Own Telemetry
-
-Rather than waiting for APM tools to reach in and grab data, libraries can proactively expose their internal operations
-using tools built directly into the runtime. The right tool for this is **Diagnostics Channels**, and more specifically,
-**Tracing Channels**. Those features are being developed by the [Node.js Diagnostics Working Group](https://github.com/nodejs/diagnostics).
-
-A huge shoutout to [Stephen Belanger](https://github.com/qard), the creator of the `diagnostics_channel` API in Node.js,
-who founded the working group and has been instrumental in pushing this topic forward. He's been providing feedback on
-proposals and acting as a voice of authority, which is sometimes exactly what's needed to convince library maintainers to get on board.
-
-### Diagnostics Channels
-
-[Diagnostics Channels](https://nodejs.org/api/diagnostics_channel.html) are a high-performance, synchronous event system built directly into Node.js. They're also supported
-in Bun, Deno, and Cloudflare Workers (via the Node.js compatibility flag), making them a cross-runtime primitive.
-
-Their primary use case is one-off events. For example, "a connection was opened" (like
-`node-redis` [does this here](https://github.com/redis/node-redis/blob/41c908e6d65419fed6d985a9664427df1f48fb98/docs/diagnostics-channel.md?plain=1#L45-L48)).
-The limitation is that they don't inherently represent a full lifecycle. You have to manually link `start` and `stop`
-events to measure duration.
-
-### Tracing Channels
-
-[Tracing Channels](https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel) solve exactly that limitation. A Tracing Channel is a bundle of related Diagnostics Channels that
-automatically creates sub-channels for a complete operation lifecycle: `start`, `end`, `error`, and `asyncStart`. More
-importantly, a `TracingChannel` automatically propagates context across async boundaries. This means APM tools can
-correlate a database query back to the incoming HTTP request that caused it, without any manual bookkeeping.
-
-Together, they give library and framework authors a standardized way to expose internal operations without coupling to
-any specific logging or tracing vendor. The library emits structured events and observability tools decide what to do
-with them.
-
-## How Libraries Can Implement Tracing Channels
-
-Tracing Channels have essentially zero cost when unused. If no subscriber is listening, emitting data costs almost
-nothing. It means library authors can add tracing channels without worrying about penalizing users who don't need
-observability. The benefits are that there is no monkey-patching needed anymore and it eliminates the need for users to
-pass `--import` flags for preloading in ESM.
-
-### Naming and Consistency: The Channel Is the Contract
-
-Tracing Channels should always be scoped to the library that emits them, using the npm package name as the namespace.
-Since package names are globally unique, this keeps channel names collision-free. For example, `mysql2` ships
-`mysql2:query` which would emit `tracing:mysql2:query:start` and all other channels. And the `unstorage` library ships
-`unstorage.get` which emits `tracing:unstorage.get:start` and so on. The [
-`untracing`](https://github.com/unjs/untracing) package is working to establish broader naming standards across the
-ecosystem.
-
-Equally important: Always emit a consistent data structure. Sentry and other APM tools can only provide automatic
-instrumentation if they know what shape your payload will have.
-
-The pattern itself is straightforward. The library wraps its operation in a `tracePromise` call:
-
-```jsx
-// Library side (e.g. inside ioredis)
-import dc from "node:diagnostics_channel";
-
-const commandChannel = dc.tracingChannel("ioredis:command");
-
-// In the command execution path:
-commandChannel.tracePromise(
- async () => {
- return await executeCommand(cmd);
- },
- { command: cmd.name, args: cmd.args },
-);
-```
-
-And on the consumer side, an SDK like Sentry subscribes to those events:
-
-```jsx
-// Consumer side (e.g. Sentry SDK)
-import dc from "node:diagnostics_channel";
-
-dc.tracingChannel("ioredis:command").subscribe({
- start(payload) {
- // create span
- },
- asyncEnd(payload) {
- // finish span
- },
- error({ error }) {
- // record error
- },
-});
-```
-
-The library and the observability tool never need to know about each other. The channel is the contract.
-
-## The Ecosystem Is Already Moving
-
-In early February 2026, we ([Andrei](https://github.com/andreiborza), [Jan](https://github.com/JPeer264) and [Sigrid](https://github.com/s1gr1d)) from Sentry
-attended [OTel Unplugged EU](https://opentelemetry.io/blog/2025/otel-unplugged-fosdem/) and brought up the topic
-“Prepare for better JS ESM Support”, which was voted on the list of top priorities for the OpenTelemetry ecosystem.
-
-
-
-So this isn't a theoretical proposal. A growing number of well-known libraries have already shipped or merged PRs for
-Diagnostics Channel and Tracing Channel support.
-
-On the framework and HTTP side, `undici` (Node.js's built-in HTTP client)
-has [shipped Diagnostics Channels](https://undici-docs.vramana.dev/docs/api/DiagnosticsChannel) since Node 20.12, and
-also `fastify` ([docs](https://fastify.dev/docs/latest/Reference/Hooks/#diagnostics-channel-hooks)), `nitro` ([PR](https://github.com/nitrojs/nitro/pull/4001)) and
-`h3` ([PR](https://github.com/h3js/h3/pull/1251)) have native support. On the database side,
-`unstorage` ([PR](https://github.com/unjs/unstorage/pull/707)) and `mysql2` ([Docs](https://sidorares.github.io/node-mysql2/docs/documentation/tracing-channels)) already use Tracing Channels,
-and `pg` / `pg-pool` are actively working on it. Redis clients aren't far behind either and already support Tracing
-Channels in `ioredis` ([PR](https://github.com/redis/ioredis/pull/2089)) and
-`node-redis` ([PR](https://github.com/redis/node-redis/pull/3195)).
-
-None of this happens without the people willing to do the work. A massive shoutout to Sentry engineer **Abdelrahman Awad** ([@logaretm](https://github.com/logaretm))
-for driving Tracing Channel implementations across multiple libraries. And
-a special thanks to **Pooya Parsa** ([@pi0](https://github.com/pi0)), his openness to collaborate in `h3` and `nitro` was
-instrumental in formalizing this approach and showing the ecosystem what it could look like.
-
-## The Vision Ahead
-
-We're still in a "chicken and egg" phase. Libraries need to add channels before APM tools have strong reasons to listen
-to them, and APM tools need to start listening before authors feel the pressure to add them.
-
-The goal is **universal JS observability**: a world where Node.js, Bun, and Deno share the same diagnostic patterns, and
-instrumentation just works without monkey-patching in CJS, without `--import` flags in ESM, and without fragile
-workarounds. Libraries become active drivers of observability ensuring they are emitting data they think is the most
-relevant to their users.
diff --git a/src/content/blog/from-users123-to-usersid-guide-to-route-parametrization.md b/src/content/blog/from-users123-to-usersid-guide-to-route-parametrization.md
deleted file mode 100644
index c8651d76..00000000
--- a/src/content/blog/from-users123-to-usersid-guide-to-route-parametrization.md
+++ /dev/null
@@ -1,130 +0,0 @@
----
-title: "From /users/123 to /users/:id: A Guide to Route Parametrization"
-date: "2025-08-12"
-tags: ["javascript", "sdk"]
-draft: false
-summary: "How Sentry's JS SDKs figure out your dynamic route names to make querying your issues easier."
-images: ["../../assets/images/from-users123-to-usersid-guide-to-route-parametrization/hero.jpg"]
-postLayout: PostLayout
-canonicalUrl: guide-to-route-parametrization
-authors: ["sigridhuemer"]
----
-
-Ever deployed a change to a dynamic route like `user/[userId]` and then checked your Sentry dashboard, only to find a huge number of differently named path names? You see they have the same problem over and over: super long trace durations, but for `/users/123`, `/users/456`, `/users/789`, and so on. You figure it’s the same root problem, but every unique URL from each user is seen as standalone, making it impossible to see the real impact.
-
-
-
-This is where **route parametrization** comes in. It’s when the Sentry SDKs figure out the original dynamic route names so the Sentry dashboard can use the parametrized route (like `users/[userId]`). This makes it possible to group the events of one dynamic page together with a single query.
-
-
-
-This article will cover different strategies our SDKs use to find the "true" route name across various frameworks.
-
-## What is a Parametrized Route, Anyway?
-
-Most frameworks and router libraries let you define **dynamic routes** with path patterns like `article/[slug]` or `article/:slug`. The Sentry SDK, however, initially grabs the full URL, like `article/hello-world`. The SDK then attempts to identify the original dynamic route, which is the **parametrized route**.
-
-## Where Parametrized Routes are Helpful
-
-Route parametrization is more than just a convenience; it fundamentally improves how you monitor and debug your applications. The **high** **cardinality** of URL-based traces—where each unique URL is treated as a distinct entity—can make it nearly impossible to identify systemic issues. Route parametrization reduces this cardinality, providing a clearer, more aggregated view of your application's health.
-
-For instance, Sentry's **Discover** and **Explore** features like the **Trace Explorer** become far more powerful. You can simply query for `/users/[userId]` to see a consolidated view. This approach gives you a complete picture of the page's performance and stability, whether you're analyzing error trends or investigating slow database queries. Furthermore, Sentry’s **Web Vitals insights** depend on the route name. By using parametrized routes, you can see aggregated Web Vitals data for a single dynamic page.
-
-
-
-## The Journey to Route Parametrization in All SDKs
-
-For a long time, automatic route parametrization in our Sentry JavaScript SDKs was only possible when a framework's router easily exposed the route name. This meant we couldn't offer a unified, consistent experience across different JavaScript frameworks and libraries. Although Sentry’s **Relay** has a best-effort route parametrization feature that uses heuristics to guess route names after events were sent, it is limited. Its dependency on heuristic means it can never be 100% certain about what was a dynamic route segment versus a static one.
-
-Over time, both our tooling at Sentry and the frameworks themselves have evolved. Frameworks introduced new APIs that gave us better access to route information. Concurrently, Sentry built more powerful tools, such as the **Sentry Bundler Plugins**, which gave us greater control over the build process. This allowed the SDKs to inject and access crucial information at build time.
-
-Now, instead of just hoping a framework's router would expose the route name, we can combine these advancements to reconstruct the "true" route name. This means you can now get consistent, high-quality route parametrization, regardless of your JavaScript stack, with new improvements available for **Next.js**, **Nuxt**, and **Astro** SDKs.
-
-## **Strategies for Finding the True Route Name**
-
-Finding the parametrized route isn't always straightforward. Our SDKs use a few clever strategies, from simple API calls to some serious detective work.
-
-### Strategy 1: Just Ask the Router
-
-In the best-case scenario, the framework's router knows the parametrized route and exposes it. For example, with `react-router`, you define a route like ``. The library makes the route object available, and the Sentry SDK can simply grab the path property to name the transaction.
-
-This is the simplest and most reliable method, as no extra processing is needed.
-
-### Strategy 2: Use a Build-Time Manifest With RegEx Matchers
-
-This approach is perfect for file-based routers like Next.js. At build time, the SDK has access to the application's entire directory structure. It uses this to create a route manifest that contains all static and dynamic routes. Some frameworks already include such a route manifest, which can directly be used by the Sentry SDKs. In the case of Next.js, the Sentry SDK creates such a manifest.
-
-Dynamic routes in the manifest include regular expressions to match against concrete URLs at runtime. For example, a request to `blog/hello-world` would be matched against the pattern `^/blog/([^/]+)$` to identify it as `/blog/:slug`. The manifest looks something like this:
-
-```jsx
-{
- staticRoutes: [
- { path: '/' },
- { path: '/blog/home' }
- ],
- dynamicRoutes: [
- {
- path: '/blog/:slug',
- regex: '^/blog/([^/]+)$',
- paramNames: ['slug'],
- },
- ]
-}
-```
-
-This manifest is bundled with the app, making route data created at build time available during runtime.
-
-### Strategy 3: Recreate the Route with Runtime Clues
-
-Sometimes, the SDK needs to play detective at runtime and reconstruct a route. In Astro, for example, our server middleware can inspect the request context. This context includes a `routes` array ([Astro type here](https://github.com/withastro/astro/blob/951897553921c1419fb96aef74d42ec99976d8be/packages/astro/src/core/app/types.ts#L56)) with information about the accessed route. For `posts/[postId]`, it looks like this:
-
-```jsx
-// Array items for route: posts/[postId]
-([{ content: "posts", dynamic: false, spread: false }],
- [{ content: "postId", dynamic: true, spread: false }]);
-```
-
-With the name and type ("dynamic" or not) of each segment, the SDK can easily stitch them back together to reconstruct the full parametrized route during runtime.
-
-### Strategy 4: Hybrid: Use Build-Time and Runtime Data together
-
-Sometimes, we need to combine information from both build and runtime to be able to reconstruct a parametrized route. Nuxt is a great example of this.
-
-During the build, the SDK gets a list of all pages and their parametrized paths:
-
-```jsx
-[
- { file: "/a/directory/pages/some/path", path: "/some/path" },
- { file: "/a/directory/pages/some/other/path", path: "/some/other/path" },
- { file: "/a/directory/pages/user/[userId].vue", path: "/user/:userId()" },
-];
-```
-
-This data is then added as a virtual file using Nuxt's `addTemplate` function. This makes the build-time route information available to the server at runtime.
-
-Then, at runtime, the Nuxt server provides a list of all modules that were used to render the incoming request:
-
-```jsx
-// SSR Context modules for user/[userId]
-Set(["app.vue", "components/Button.vue", "pages/user/[userId].vue"]);
-```
-
-Neither piece of information is enough on its own. The build-time data has the parametrized path we want, but we don't know which one to pick. The runtime data tells us which page file was used, but not its parametrized path. By matching the file path from the runtime context against the list from the build, the SDK can confidently identify the correct parametrized route.
-
-## **Bridging Environments: From Server to Client**
-
-Often, the route is discovered on the server but is needed to instrument page loads in the browser. For Server-Side Rendered (SSR) pages, the server can determine the parametrized route and inject it directly into the HTML `` using a meta tag.
-
-```html
-
-```
-
-When the Sentry SDK initializes in the browser, it reads this meta tag to correctly name the initial page-load transaction.
-
-## Conclusion: Smarter Issue Querying, Faster Fixes
-
-Route parametrization is the key to turning a noisy, overloaded issue stream into a clean, actionable list. By converting specific URLs like `/users/123` into their generic templates like `/users/[id]`, you can query errors and performance issues intelligently.
-
-Our SDKs achieve this using a variety of techniques tailored to each framework, whether it's by asking the router directly, using a build-time manifest, or combining clues from different stages of the application lifecycle. Ideally, the frameworks already provide easy access to the parameterized route name.
-
-The result? Spending less time trying to figure out the impact of an issue and more time fixing it.
diff --git a/src/content/blog/getting-started-with-jetpack-compose.md b/src/content/blog/getting-started-with-jetpack-compose.md
deleted file mode 100644
index 951a5f10..00000000
--- a/src/content/blog/getting-started-with-jetpack-compose.md
+++ /dev/null
@@ -1,368 +0,0 @@
----
-title: "Getting Started with Jetpack Compose"
-date: "2023-02-15"
-tags: ["mobile", "jetpack compose", "android"]
-draft: false
-summary: Jetpack Compose, a new declarative UI toolkit by Google made for building native Android apps, is rapidly gaining traction. The main advantage of using Jetpack Compose is that it allows you to write UI code that is more concise and easier to understand. This leads to improved maintainability and reduced development time. The main advantage of using Jetpack Compose is that it allows you to write UI code that is more concise and easier to understand. This leads to improved maintainability and reduced development time.
-images: [../../assets/images/getting-started-with-jetpack-compose/jetpackcompose-hero.jpg]
-postLayout: PostLayout
-canonicalUrl: https://blog.sentry.io/2023/02/15/getting-started-with-jetpack-compose/
-authors: ["lazarnikolov"]
----
-
-Recently, we wrote about the [demonstrative move to declarative UI](/blog/mobile-the-future-is-declarative/). With [Jetpack Compose](http://d.android.com/compose), Android is joining the declarative trends.
-
-Jetpack Compose, a new declarative UI toolkit by Google made for building native Android apps, is rapidly gaining traction. In fact, as announced at the [Android Dev Summit](https://www.youtube.com/watch?t=1069&v=Awi4J5-tbW4&feature=youtu.be) last year last year, 160 of the top 1,000 Android apps already use Jetpack Compose. In contrast to the traditional XML Views, Jetpack Compose allows you to build UIs using composable functions that describe how the UI should look and behave.
-
-The main advantage of using Jetpack Compose is that it allows you to write UI code that is more concise and easier to understand. This leads to improved maintainability and reduced development time.
-
-The main disadvantage of using Jetpack Compose is that it’s relatively new, so its ecosystem is limited and the number of available libraries, tools, and resources is lower than the traditional ecosystem.
-
-Despite that, we believe that learning Jetpack Compose is worth the learning curve and challenges. Here are some tips we’ve found helpful as you are getting started.
-
-## How to start using Jetpack Compose
-
-The recommended IDE for working with Jetpack Compose is [Android Studio](https://developer.android.com/studio). After downloading and installing Android Studio, you’ll get the option to create a new project. To create a new Jetpack Compose application, you need to select either the `Empty Compose Activity` (which uses Material v2), or `Empty Compose Activity (Material3)` (which uses the Material v3 which is in version 1.0 as of last year). You can see both options in the top right of this screenshot:
-
-
-
-This is the easiest way to get started with Jetpack Compose. If you’d like to enable Jetpack Compose into an existing Android application, here’s what you need to do:
-
-1. Add the following build configurations in your app’s `build.gradle` file:
-
-```java
- android {
- buildFeatures {
- // this flag enables Jetpack Compose
- compose true
- }
-
- composeOptions {
- // the compiler version should match
- // your project's Kotlin version
- kotlinCompilerExtensionVersion = "1.3.2"
- }
-}
-```
-
-2. Add the Compose BOM ([Bill of Materials](https://developer.android.com/jetpack/compose/bom/bom)) and the subset of Compose dependencies to your dependencies:
-
-```java
- dependencies {
- def composeBom = platform('androidx.compose:compose-bom:2023.01.00')
- implementation composeBom
- androidTestImplementation composeBom
-
- // Choose one of the following:
- // Material Design 3
- implementation 'androidx.compose.material3:material3'
- // or Material Design 2
- implementation 'androidx.compose.material:material'
- // or skip Material Design and build directly on top of foundational components
- implementation 'androidx.compose.foundation:foundation'
- // or only import the main APIs for the underlying toolkit systems,
- // such as input and measurement/layout
- implementation 'androidx.compose.ui:ui'
-
- // Android Studio Preview support
- implementation 'androidx.compose.ui:ui-tooling-preview'
- debugImplementation 'androidx.compose.ui:ui-tooling'
-
- // UI Tests
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
- debugImplementation 'androidx.compose.ui:ui-test-manifest'
-
- // Optional - Included automatically by material, only add when you need
- // the icons but not the material library (e.g. when using Material3 or a
- // custom design system based on Foundation)
- implementation 'androidx.compose.material:material-icons-core'
- // Optional - Add full set of material icons
- implementation 'androidx.compose.material:material-icons-extended'
- // Optional - Add window size utils
- implementation 'androidx.compose.material3:material3-window-size-class'
-
- // Optional - Integration with activities
- implementation 'androidx.activity:activity-compose:1.5.1'
- // Optional - Integration with ViewModels
- implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
- // Optional - Integration with LiveData
- implementation 'androidx.compose.runtime:runtime-livedata'
- // Optional - Integration with RxJava
- implementation 'androidx.compose.runtime:runtime-rxjava2'
-
-}
-```
-
-## How do you build UI in Jetpack Compose?
-
-Jetpack Compose uses Composables to define the view hierarchy, and modifier to apply visual appearance and behavior changes to the composables they’re added to.
-
-### Composable functions
-
-Composable functions (or just Composables) are ordinary Kotlin functions that are annotated with `@Composable`, can be nested within another composable functions, and return a hierarchy of other composables in order to define their UI. Let’s see a simple composable that defines a contact row UI that contains a user photo, and a name and phone number:
-
-```java
-@Composable
-fun ContactRow(user: User) {
- Row {
- Image (
- painter = painterResource(id = R.drawable.user),
- contentDescription = "A photo of a user"
- )
-
- Column {
- Text(user.name)
- Text(user.phone)
- }
- }
-}
-```
-
-The `Row` composable is a layout composable that renders its children one next to another. The `Image` composable is the first child which is going to render the `user` drawable. Then we have the `Column` composable which, similar to the `Row`, is a layout composable, but it renders its children one below another. The children of the `Column` composable are two `Text` composables that render the user’s name and phone number.
-
-### Modifiers
-
-Modifiers are used to change the visual appearance and behavior of the composables they’re added to. We use modifiers when we want to change UI elements such as the size of the composable (width, height), the padding, background, or alignment.
-
-Modifiers can also be stacked on top of each other, allowing us to modify multiple visual properties. Here’s an example of how we can set the padding and max width of the Contact row from the previous snippet:
-
-```java
-@Composable
-fun ContactRow(user: User) {
- Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
- ...
- }
-}
-```
-
-## How do you interact with data in Jetpack Compose?
-
-There are multiple ways to keep data within your Jetpack Compose app: [MutableState](https://volcano-bovid-81c.notion.site/Getting-Started-with-Jetpack-Compose-7187d91a2f0c4e56969db56c51c91ec1), [LiveData](https://volcano-bovid-81c.notion.site/Getting-Started-with-Jetpack-Compose-7187d91a2f0c4e56969db56c51c91ec1), and [StateFlow](https://volcano-bovid-81c.notion.site/Getting-Started-with-Jetpack-Compose-7187d91a2f0c4e56969db56c51c91ec1).
-
-### MutableState
-
-In Jetpack Compose, state management can be accomplished by using the `remember` API to store an object in memory, and the `mutableStateOf` to declare a state variable. We can store both mutable and immutable objects. The `mutableStateOf` creates an observable `MutableState`, which is an observable type.
-
-```java
-interface MutableState : State {
- override var value: T
-}
-```
-
-Any changes to value `schedules` a recomposition (re-rendering) of any composable functions that read it. There are three ways to declare a `MutableState` object:
-
-- `val mutableState = remember { mutableStateOf(0) }`
-- `var value by remember { mutableStateOf(false) }`
-- `val (value, setValue) = remember { mutableStateOf("Hello, Compose!") }`
-
-### LiveData
-
-LiveData is a data holder class that can be observed within a given lifecycle, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This ensures LiveData only updates observers that are in an active lifecycle state, which also ensures no memory leaks happen within your app.
-
-Let’s see an example of working with LiveData:
-
-1. You need to create an instance of the `LiveData` class to hold a certain type of data, which is usually done within your [ViewModel](https://developer.android.com/reference/androidx/lifecycle/ViewModel) class (use `MutableLiveData` if you’d like to update the value at some point):
-
-```java
-class HomeViewModel : ViewModel() {
- // Create a MutableLiveData instance that keeps a string
- val userName = MutableLiveData()
-}
-```
-
-2. Obtain the value in your composable by calling the `observeAsState` method:
-
-```java
-@Composable
-fun HomeScreen (viewModel: HomeViewModel = viewModel()) {
- // Create an observer of the state of userName
- val userName = viewModel.userName.observeAsState()
-
- // Use the value in your UI
- Column {
- Text(userName)
- }
-}
-```
-
-3. To update `userName`’s value (also usually done in the view model), create a function that sets the new `value` to its value property:
-
-```java
-fun updateUserName(newName: String) {
- userName.value = newName
-}
-```
-
-4. You’d use the new function in your Compose file as `viewModel.updateUserName("...")`.
-
-### StateFlow
-
-StateFlow is a newer alternative to LiveData. Both have similarities, and both are observable. Here’s how you can work with StateFlow in Jetpack Compose:
-
-1. Create an instance of `StateFlow` to hold a certain type of data (use `MutableStateFlow` if you’d like to update the value at some point)
-
-```java
-class HomeViewModel : ViewModel() {
- // Create a MutableStateFlow instance that keeps a string
- val userName = MutableStateFlow()
-}
-```
-
-2. Obtain the value in your composable by calling the `collectAsState` method:
-
-```java
-@Composable
-fun HomeScreen (viewModel: HomeViewModel = viewModel()) {
- // Create an observer to collect the state of userName
- val userName = viewModel.userName.collectAsState()
-
- // Use the value in your UI
- Column {
- Text(userName)
- }
-}
-```
-
-3. To update `userName`’s value (also usually done in the view model), create a function that sets the new value to its `value` property:
-
-```java
-fun updateUserName(newName: String) {
- userName.value = newName
-}
-```
-
-4. You’d use the new function in your Compose file as `viewModel.updateUserName("...")`.
-
-## What are the best practices for Jetpack Compose?
-
-Aside from the official best practices documentation, we’ve got a few additional tips that would make your codebase safer and easier to work in.
-
-### Code organization
-
-Every developer or organization has their own opinions on how a project should be structured. There is no “right” or “wrong” way to do it. Okay, maybe it’s wrong to put every file in one single directory 😅. Here’s an example structure to help you get started, which you can modify and evolve as your project grows:
-
-```bash
-.
-├─ 📁 **ui** (to keep all your UI related things)
-| ├─ 📁 **screens** (where you define your screens composables and their corresponding view models)
-| | └─ 📁 **home**
-| | ├─ 📝 **HomeScreen.kt** (the UI for the Home screen)
-| | └─ 📝 **HomeViewModel.kt** (the view model for the Home screen)
-| ├─ 📁 **components** (where you define components that are shared across multiple screens)
-| | └─ 📝 **UserList.kt**
-| └─ 📁 **theme** (where you keep your theme definition and design tokens)
-| ├─ 📝 **Colors.kt**
-| ├─ 📝 **Shapes.kt**
-| ├─ 📝 **Theme.kt**
-| └─ 📝 **Typography.kt**
-├─ 📁 **utils** (where you keep your various utility functions, like data converters etc...)
-| └─ 📝 **DateUtils.kt**
-└─ 📝 **MainActivity.kt** (this is your default MainActivity)
-```
-
-### Avoid creating “god” files
-
-“God” files are a big no-no. They’re files that contain all code associated with them: UI, domain, business logic, utility functions etc… It might be easier putting everything into one file, but maintaining that would get harder and harder as you add functionalities. The solution to this is using a proper architecture in your Jetpack Compose app.
-
-There are multiple architectures that you can use, all with their own pros and cons. The most common one in Jetpack Compose is MVVM, abbreviated from Model-View-ViewModel, because Jetpack Compose has a first-class [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) implementation.
-
-### Stay true to the MVVM
-
-As you saw from the previous examples, Jetpack Compose has a first-class [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) implementation. The MVVM, or Model-View-ViewModel, is a software design pattern that is structured to separate business logic from the UI. That means, your UI should not handle state updates, but it should let the view model do that by sending it user actions.
-
-Let’s explore that with an example. Remember the `MutableStateFlow` example from before? That example was oversimplified on purpose, but in a real-world project you would never expose a `MutableStateFlow` from your `ViewModel`, but just a `StateFlow`. In order to make that work, you should define a private `MutableStateFlow` variable and a public `StateFlow` variable that returns the mutable flow by invoking the `asStateFlow()` method.
-
-```java
-class HomeViewModel : ViewModel() {
- // Create a private MutableStateFlow instance that keeps a string
- private val _userName = MutableStateFlow()
-
- // Create a public StateFlow that returns the MutableStateFlow as immutable
- val userName: StateFlow = _userName.asStateFlow()
-}
-```
-
-With this simple change, we’re preventing the UI from being able to change the state. But, how do we actually change the state? We’ll expose a function from the view model that does that!
-
-```java
-class HomeViewModel : ViewModel() {
- private val _userName = MutableStateFlow()
- val userName: StateFlow = _userName.asStateFlow()
-
- // Create a public function that updates the private MutableStateFlow value
- fun setUserName(newName: String) {
- _userName.value = newName
- }
-}
-```
-
-So now the UI has an immutable `StateFlow` that it can observe, and a function to update its value. The business logic lives inside of the view model, while the Composable is only responsible to react to state changes and send user actions to the view model.
-
-### Don’t create a thousand flows
-
-So you’ve learned how to create state flows. Great! Would you repeat the same for every state variable you need in your UI? Please don’t 😅 To avoid that, you can create a `data class` that keeps all of the values of your state, and create a single flow that uses it.
-
-Let’s learn this with an example. If we wanted to also keep the user’s phone number, email and address, we can create a data class called `HomeScreenState` that contains all those values:
-
-```java
-data class HomeScreenState(
- val userName: String = ""
- val userPhone: String = ""
- val userEmail: String = ""
- val userAddress: String = ""
-)
-```
-
-Then we would refactor our view model to use the new `HomeScreenState` instead of a `String`:
-
-```java
-class HomeViewModel : ViewModel() {
- private val _uiState = MutableStateFlow()
- val uiState: StateFlow = _uiState.asStateFlow()
-
- // ...
-}
-```
-
-And then we can use all of the values in our composable by `viewModel.uiState.userName`. If we also wanted to be able to update all those values, we would create functions for each of them in our view model:
-
-```java
-class HomeViewModel : ViewModel() {
- private val _uiState = MutableStateFlow()
- val uiState: StateFlow = _uiState.asStateFlow()
-
- fun updateUserName(newName: String) {
- _uiState.update {
- it.copy(
- userName = newName
- )
- }
- }
-
- fun updateUserEmail(newEmail: String) {
- _uiState.update {
- it.copy(
- userEmail = newEmail
- )
- }
- }
- }
-
- // ...
-
-}
-```
-
-### Keep a close eye on your errors and performance in production
-
-As you’re getting acclimated to Jetpack Compose, an error and performance monitoring tool can be really helpful to reduce your learning curve and ensure that your app is bug-free. Jetpack Compose does a lot of heavy lifting for developers – as a declarative toolkit, developers need to write less code to describe their UI, and Jetpack Compose takes care of the rest. But it does abstract away a lot of code, making it difficult to identify errors.
-
-[Sentry](https://sentry.io/for/android/) offers an out-of-the-box integration that can help you build a better Jetpack Compose app. The integration gives precise context to reduce troubleshooting time with transactions and breadcrumbs. Keep an eye on all the issues and crashes your app is experiencing in production, with a lot of context as to why the issue happened, the exact line of code that triggered it, and all sorts of hardware and software info of the device it ran.
-
-
-
-## Conclusion
-
-I’d totally understand if you’re feeling overwhelmed by now, but let’s do a quick recap! We’ve learned how to create a new Jetpack Compose project, and that Jetpack Compose uses Composables and Modifiers to define the view hierarchy and apply visual changes. Data in Jetpack Compose can be handled either with a `MutableState`, `LiveData`, or `StateFlow`, which make the composables that observe it re-render when the value changes, making our UI dynamic. We also learned how to keep our projects tidy, and how to write maintainable composables and view models.
-
-Even though it’s a relatively new technology, Jetpack Compose’s ecosystem is steadily growing, so we can expect to see a lot of libraries pop up that make it easier to create Jetpack Compose apps. With companies like Lyft, Twitter, Airbnb, Square, Reddit, and Firefox putting their trust into it, more and more developers will follow along and create apps, libraries and resources for Jetpack Compose.
diff --git a/src/content/blog/how-open-pr-comments-work.md b/src/content/blog/how-open-pr-comments-work.md
deleted file mode 100644
index c798ec71..00000000
--- a/src/content/blog/how-open-pr-comments-work.md
+++ /dev/null
@@ -1,102 +0,0 @@
----
-title: "How open PR comments work"
-date: "2024-04-18"
-tags: ["integrations", "github", "pr-comments"]
-draft: false
-summary: "This is how we manage to comment Sentry issues related to code you're modifying in a pull request within seconds."
-images: [../../assets/images/open-pr-comments/hero.jpeg]
-postLayout: PostLayout
-canonicalUrl: open-pr-comments
-authors: ["cathyteng"]
----
-
-At Sentry, we always want to bring value to the developer. One area we can do this is through developer workflows, such as pull requests. If you’re already working in a particular area, it’s useful to be informed about issues related to the code you’re changing so you can be proactive in acknowledging and addressing them.
-
-When somebody opens a GitHub PR, we parse the functions from the PR diff, search for unresolved, unhandled issues related to those functions in the PR files, and make a nice little comment on that PR.
-
-# How it works
-
-
-
-## Webhook
-
-When somebody installs the [Sentry GitHub App](https://github.com/apps/sentry-io), we request permissions so that we can consume webhooks for particular events that we would like to handle. We have a webhook for pull request events that does stuff on the Sentry side when somebody interacts with a pull request on GitHub. So when somebody opens a pull request, we can kick off the open PR comment workflow.
-
-## Comment workflow
-
-### Qualification checks
-
-We fetch information for the PR files from GitHub, and then ensure that we don’t comment on PRs that have any of the following:
-
-1. More than 7 files modified
-2. More than 500 lines changed
-
-We also don't count files that are:
-
-1. Deleted, because we cannot extract function information due to the GitHub API does not giving us a patch
-2. New, since they won’t have any issues associated with them
-
-Why do we do this? At a certain point, when a PR is touching so many lines and/or files, it becomes less and less useful to point out specific issues related to the functions inside the PR. For instance, if somebody applies a linting change, it could result in a lot of lines modified.
-
-Note that we only support a particular set of languages (via looking at file extensions). We skip counting files and lines if they don’t have a file extension we support, and only continue with the files that we can support if these checks are met.
-
-### Fetching issues for each file
-
-#### Reverse codemapping
-
-1. Normal [codemappings](https://blog.sentry.io/code-mappings-and-why-they-matter/) map a file in the stack trace (within Sentry) to the source code (in a source code management integration such as GitHub) using a stack trace root and a source code root. Sentry may store file names differently than in GitHub, so codemappings store this relationship. We leverage codemappings here because we want to figure out the Sentry project(s) associated with the file and the stored name of the file in order to make a [Snuba](https://blog.sentry.io/introducing-snuba-sentrys-new-search-infrastructure/) query, which requires `project_id`.
-2. We use the organization, the repository, and the file name to attempt to fetch code mappings for the file, matching on whether any source code root for a codemapping is a substring of the file name. If any codemappings are found, we reverse codemap by replacing the source code root in the file name from GitHub with the stack trace root. (Reverse because usually we go from stack trace to source code root, for instance when opening up a line from a stack trace in GitHub)
-
-#### Extract functions from the file patch
-
-1. Depending on the language of the file, we fetch the appropriate parser and apply it to the file patch (the `git diff` for the file).
-2. This applies a regex to the whole file, looking for git hunk headers (e.g. `@@ -188,9 +188,7 @@ def __init__():` for Python) that indicate a function in the language, and that the code modified in the section below it belongs to that function. This is more or less correct in finding functions being modified in a PR, unless the function is super short or the lines modified are near the top of the function (this is a limitation of git).
-3. For Python, the regex only looks for `def {function_name}.` For JavaScript/TypeScript, there are more ways to initialize a function (function declaration, arrow function, function expression, etc), so there are multiple regexes to find function names.
-
-#### Snuba query to fetch top 5 issues by count
-
-1. We first fetch the first 10k unresolved issues for the projects found via reverse codemapping through Postgres, ordered by times seen. This is to prevent overloading the Snuba query if we pass in too many issue ids.
-2. Next, we have a complicated looking Snuba query that does the following:
- - Subquery to fetch the count of events for each issue id
- - Query that filters on the subquery to 1) squash issue ids with the same title and culprit, 2) look for unhandled events that have the file name+function combo for any of the file’s function within the first 4 frames of the stacktrace, and 3) return the top 5 issues with the greatest count of events
-3. There is also some different logic being inserted depending on the language of the file. For instance, for JavaScript/TypeScript we also look for events that have `{any classname}.function_name` inside the file in addition to just `function_name` inside the file because of how JavaScript/TypeScript events are stored.
-
-### Create comment
-
-Then for each file, we make a little table with the function and issue information. All the tables besides the one for the first file are hidden in a toggle. Each file type may also have a slightly different formatting template. In JavaScript/TypeScript, we want to show the `Affected Users` because it’s more important for frontend. Meanwhile, Python usually always has 0 affected users so it’s not shown.
-
-
-
-## A note on language parsers
-
-We support different languages in open PR comments. However, they also require different methods to extract functions from the diff, different handling for the ways events with those functions might be stored in Snuba, and how to format the comment table for each file type. The current list of support languages can be found in our [docs](https://docs.sentry.io/product/integrations/source-code-mgmt/github/#open-pull-request-comments).
-
-Each file extension that we support is mapped to a language parser.
-
-If we find more things that are different between languages, we can add to the parser classes. So far we have:
-
-- Issue row template for the comment
-- Extracting functions from patch
-- [multiIf](https://clickhouse.com/docs/en/sql-reference/functions/conditional-functions#multiif)
- - This can contain custom logic to fetch the function name from the stackframe that matches the file name + a name within the list of function names.
- - We do this because we can match up to X frames deep in the stacktrace, we might have a set of function names we’re matching on, and we want the actual function name that we matched on in the stack trace. The stack trace is stored as an array.
-
-### Example language parser
-
-Language parser base class ([code](https://github.com/getsentry/sentry/blob/ffe0d41533b21ec6a448048e3ba43b16a491ea07/src/sentry/tasks/integrations/github/language_parsers.py#L65-L168))
-
-Example implementation for JavaScript ([code](https://github.com/getsentry/sentry/blob/ffe0d41533b21ec6a448048e3ba43b16a491ea07/src/sentry/tasks/integrations/github/language_parsers.py#L212-L240))
-
-# How we got here
-
-I also wanted to call out that getting here was no easy feat. We also investigated using abstract syntax trees (ASTs) to be completely sure what functions had been modified. However, the tradeoff would be that we would need to 1) hit more GitHub APIs to fetch the complete files and the files for the base commit for the PR and 2) construct or borrow AST logic for each language to make the comparison.
-
-We decided to iterate from the simplest implementation possible in case it became apparent that the project was not worth pursuing. we first iterated internally with only file-level granularity for these open PR comments. This was not received well, so we knew that we needed to implement open PR comments with function-level granularity.
-
-# How do I get these?
-
-If your project is written in a language that [we currently support](https://docs.sentry.io/product/integrations/source-code-mgmt/github/#open-pull-request-comments), and you use GitHub, navigate to the GitHub integration features and toggle `Enable Comments on Open Pull Requests`!
-
-
-
-Open PR Comments is currently only available on GitHub, and we are looking to extend all the PR comment features to other SCM integrations soon (GitLab, Bitbucket, etc). For more feature requests, submit an issue to the Sentry repo or comment on the PR comment GitHub discussion [https://github.com/getsentry/sentry/discussions/49996](https://github.com/getsentry/sentry/discussions/49996)
diff --git a/src/content/blog/how-sentry-queries-unstructured-data-in-clickhouse-62x-faster.md b/src/content/blog/how-sentry-queries-unstructured-data-in-clickhouse-62x-faster.md
deleted file mode 100644
index 5c0072b6..00000000
--- a/src/content/blog/how-sentry-queries-unstructured-data-in-clickhouse-62x-faster.md
+++ /dev/null
@@ -1,184 +0,0 @@
----
-title: "How Sentry queries unstructured data in ClickHouse 62x faster"
-date: "2025-03-24"
-tags: ["clickhouse", "web", "optimization"]
-draft: false
-summary: "We repurposed a hashtable to make ClickHouse significantly faster for analytical queries"
-images: [../../assets/images/how-sentry-queries-unstructured-data-in-clickhouse-62x-faster/hero.png]
-postLayout: PostLayout
-canonicalUrl:
-authors: [colinchartier]
----
-
-Sentry’s users send us many billions of ‘spans’ to measure the performance of their products - these are essentially a measurement of how long a particular operation took.
-
-
-
-Since our users are so varied, we can’t assume anything about the structure of the spans they send. They might send one which corresponds to how long a webpage took to load, and one which measures how long a phone call took.
-
-We need to handle completely _unstructured_ data.
-
-```jsx
-// Example: A user sends a span corresponding to a webpage navigation
-{
- "start_time": "5:01 on Monday",
- "end_time": "5:02 on Monday",
- "name": "React webpage navigation",
- "attributes": {
- "browser": "chrome",
- "reactVersion": "21.3.0"
- }
-}
-
-// Example: A user sends a span corresponding to a phone call
-{
- "start_time": "5:01 on Tuesday",
- "end_time": "5:10 on Tuesday",
- "name": "Inbound phone call",
- "attributes": {
- "destinationNumber": "+1 800‑555‑0115",
- "callResolved": true
- }
-}
-
-// These two examples share no attributes! They are *unstructured*
-```
-
-## How spans work at sentry
-
-Sentry has three core use-cases of spans:
-
-1. **Alerting** - “Email me when the average visitor is taking more than a second to load the webpage”
-2. **Graphing** - “Give me a graph of how long the webpage took to load for Chrome users over the past week”
-3. **Tracing** - “Colin at Sentry emailed me, and said he got an error on our pricing page. Find me all of the spans and errors corresponding to when Colin visited our pricing page.”
-
-All of these use-cases are fulfilled by an OSS database called [ClickHouse](https://clickhouse.com/) via a service we run called [Snuba](https://getsentry.github.io/snuba/).
-
-They are all relatively hard to implement, but the second is particularly hard - making millions of graphs with billions of data points is inherently a very expensive thing.
-
-
-
-## The problem with unstructured data
-
-ClickHouse was originally made to store structured data - in particular, it was originally designed to be the database for an analytics tool. In that original use-case, the data had a very rigid schema:
-
-```sql
-CREATE TABLE clicks(pos_x UInt(16), pos_y UInt(16), user_id UInt(64), left_click Boolean, ...)
-```
-
-Having a schema lets ClickHouse optimize how it stores the data - in particular, it creates a file for each column `pos_x.dat`, `pos_y.dat`, … - and only reads the few files corresponding to any particular query.
-
-However, for our use-case, the `attributes` field contains arbitrary user-provided keys - things like `callResolved` and `browser` which are on some spans but not others. This means that there would be thousands and thousands of files if we did things the naive ClickHouse way - `callResolved.dat`, `browser.dat`, `destinationNumber.dat`, …
-
-```sql
-CREATE TABLE spans_v1(id UInt(64), browser String, mobile_device String, duration_ms Float(64), (1000+ other columns))
-```
-
-We tried this - and it immediately failed. ClickHouse allocates memory for every existing column for every row you insert. If you have 1000 columns, it will allocate hundreds of gigabytes for every insertion! Not to mention, if a user sends a new column that we’ve never seen before, we still can’t store it in one of these columns.
-
-## The `Map` type in ClickHouse
-
-Luckily for us, ClickHouse comes with a special type called `Map` . We could use it for our spans table to avoid adding thousands of columns!
-
-```sql
-CREATE TABLE spans_v2(id UInt(64), attributes_string Map(String, String), attributes_float Map(String, Float(64)))
-```
-
-With this schema, we could write queries which reference unstructured data: `SELECT sum(attributes_float['duration_ms']) WHERE attributes_string['os']='chrome'`
-
-Here, we only have three columns instead of the thousands+ in `spans_v1` from earlier.
-
-### Bad performance 😟
-
-There’s a problem with this structure too - we’re only using 3 files! If you try to read `attributes_float['duration_ms']`, you are opening a single massive file which contains every single numeric attribute, and then looping over all of its bytes.
-
-| | spans_v1 | spans_v2 |
-| ------------ | -------- | -------- |
-| # of columns | 1000+ | 3 |
-
-That means that if your span has 30 attributes, we have to load the data from all 30 of those attributes to just aggregate a single one - this is essentially defeating the whole point of ClickHouse, and makes things feel much slower!
-
-## A digression to hash tables
-
-There’s a common data structure in computer science that’s over 70 years old - the _Hash Table._
-
-The (simplified) idea for a hash table is to have a fixed set of buckets, where each bucket has a few elements. A function called the _hash_ converts the key to a number, and then we use that number to decide which bucket the element goes into.
-
-Consider the following python pseudocode:
-
-```python
-class HashTable:
- def __init__(self, num_buckets=30):
- self.buckets = [[] for _ in range(num_buckets)]
-
- def insert(item_key: str, item_val: Any):
- hash_val = hash(item_key)
- self.buckets[hash_val % len(self.buckets)].append((item_key, item_val))
-
- def get(item_key: str):
- hash_val = hash(item_key)
- bucket_item_might_be_in = self.buckets[hash_val % len(self.buckets)]
- for k, v in bucket_item_might_be_in:
- if k == item_key:
- return v
-
-```
-
-If you have ~30 buckets and ~30 unique keys, then on average each column will have a single key in it.
-
-## A hash table in ClickHouse
-
-This idea leads us to a third version of the schema:
-
-```sql
-CREATE TABLE spans_v3(
- id UInt(64),
- attributes_string_0 Map(String, String),
- attributes_string_1 Map(String, String),
- ...
- attributes_string_49 Map(String, String),
- attributes_float_0 Map(String, Float(64)),
- attributes_float_1 Map(String, Float(64)),
- ...
- attributes_float_49 Map(String, Float(64)),
-)
-```
-
-You might already see the parallels with the hash table above - we’ve split the keys in the second approach into 100 buckets using a hash function, and now each column has ~1/100th of the total data.
-
-We can then write [a query processor](https://github.com/getsentry/snuba/blob/2525fb711585104f8b9d88fd1b9ae96726b29e4e/snuba/clickhouse/translators/snuba/mappers.py#L231-L264) in Snuba which takes an incoming request, and transforms it to refer to specific buckets, instead of a single big column:
-
-```python
-# fnv_1a is a fast hash function often used in hash tables
-bucket_idx = fnv_1a(key.value.encode("utf-8")) % self.num_attribute_buckets
-
-# consider a request for attributes_string['hello'].
-# fnv_1a('hello') is 1335831723
-# self.num_attribute_buckets is 50
-# so fnv_1a(key.value) % self.num_attribute_buckets is 3
-# which means the final request goes to attributes_string_3['hello']
-return arrayElement(
- expression.alias,
- ColumnExpr(None, self.to_col_table, f"{self.to_col_name}_{bucket_idx}"),
- key,
-)
-```
-
-The end result of this schema is that we have a bounded number of columns (~100) where each column has approximately ~1/100th of the total data, making every query scan approximately 1% of the data of our second approach, and making every query approximately ~100x faster!
-
-## Benchmarks
-
-This didn’t all happen in a vacuum - we carefully defined every operation we wanted to optimize with our new schema, and benchmarked throughout the development process. Here is a summarized version of our findings:
-
-| | Get newest trace that matches span conditions | Get newest trace that matches span conditions | Get spans for trace | OLAP WHERE sentry_tags[x] for a specific project |
-| -------- | --------------------------------------------- | --------------------------------------------- | ------------------- | ------------------------------------------------ |
-| spans_v2 | 4.076 sec | 0.032 sec. | 0.016 sec | 2.643 sec. |
-| spans_v3 | 2.334 sec. | 0.018 sec. | 0.021 sec | 0.042 sec. |
-
-The particular operation we were worried about (OLAP - the `sum(attrs['x']) WHERE attrs['y']` query above) was 62x faster with the bucketing schema, which roughly matches the performance gains we expected.
-
-## Conclusion
-
-Hash Tables are a very old concept in Computer Science, and it’s easy to discount such things as purely academic.
-
-By applying them to a domain where they aren’t traditionally used, we were able to dramatically improve our database performance without needing to make large code changes. This schema is now what is powering Sentry’s latest features: Custom dashboards, log storage, tracing, and more!
diff --git a/src/content/blog/how-to-mutate-data-in-a-system-designed-for-immutable-data.md b/src/content/blog/how-to-mutate-data-in-a-system-designed-for-immutable-data.md
deleted file mode 100644
index 7571bf2b..00000000
--- a/src/content/blog/how-to-mutate-data-in-a-system-designed-for-immutable-data.md
+++ /dev/null
@@ -1,191 +0,0 @@
----
-title: "How to Mutate Data in a System Designed for Immutable Data"
-date: "2019-10-25"
-tags: ["clickhouse", "databases", "building sentry"]
-draft: false
-summary: "Sentry’s growth led to increased write and read load on our databases, and, even after countless rounds of query and index optimizations, we felt that our databases were always a hair’s breadth from the next performance tipping point or query planner meltdown. Increased write load also led to increased storage requirements (if you’re doing more writes, you’re going to need more places to put them), and we were running what felt like an inordinate number of servers with a lot of disks for the data they were responsible for storing. Here’s a look at how we attempted to understand which database system was right for us and how we adapted our approach when we encountered some unexpected challenges."
-images: [../../assets/images/how-to-mutate-data-in-a-system-designed-for-immutable-data/hero.gif]
-postLayout: PostLayout
-canonicalUrl: https://blog.sentry.io/2019/10/25/how-to-mutate-data-in-a-system-designed-for-immutable-data/
-authors: ["filippopacifici", "jamescunningham", "tedkaemming"]
----
-
-_Welcome to our series of blog posts about things [Sentry does that perhaps we shouldn’t do](/tags/building-sentry). Don’t get us wrong — we don’t regret our decisions. We’re sharing our notes in case you also choose the path less traveled. In this post, we look at how decisions made around prioritizing — or, as in our case, deprioritizing — mutability and consistency (in an [ACID](https://en.wikipedia.org/wiki/ACID) sense) affect database performance and how we deal with the fact that our data is mostly — but not totally — immutable._
-
-In [another post published here earlier this year](/blog/introducing-snuba-sentrys-new-search-infrastructure), we described some of the decision making that went into the design and architecture of Snuba, the primary storage and query service for Sentry’s event data. This project started out of necessity; months earlier, we discovered that the time and effort required to continuously scale our existing PostgreSQL-based solution for indexing event data was becoming an unsustainable burden.
-
-Sentry’s growth led to increased write and read load on our databases, and, even after countless rounds of query and index optimizations, we felt that our databases were always a hair’s breadth from the next performance tipping point or query planner meltdown. Increased write load also led to increased storage requirements (if you’re doing more writes, you’re going to need more places to put them), and we were running what felt like an inordinate number of servers with a lot of disks for the data they were responsible for storing. We knew that something had to change.
-
-Here’s a look at how we attempted to understand which database system was right for us and how we adapted our approach when we encountered some unexpected challenges.
-
-## Outgrowing PostgreSQL
-
-We knew that PostgreSQL wasn’t the right tool for this job, and many of the features that it provides — such as ACID transactions, MVCC semantics, and even row-based mutations — were ultimately unnecessary for the kinds of data that we were storing in it, as well as the types of queries we were running. In fact, not only were they unnecessary, but they caused performance issues at best, and [had played a major role in our worst outage to date](https://blog.sentry.io/2015/07/23/transaction-id-wraparound-in-postgres/) at worse. We can’t say that PostgreSQL was the problem — it served us well for years, and we still happily use it in many different parts of our application and infrastructure today without any intention of removing it — it just wasn’t the right solution for the problems we were facing any longer.
-
-We realized that we needed a system oriented around fast aggregations over a large number of rows, and one optimized for bulk insertion of large amounts of data, rather than piecemeal insertion and mutation of individual rows.
-
-## ClickHouse: faster queries + predictable performance
-
-Ultimately, after evaluating several options, we settled on [ClickHouse](https://clickhouse.yandex/), which is the database that currently underpins Snuba, our service for storing and searching event data. ClickHouse and PostgreSQL have very different architectures (some of which we’ll dive into more detail about a bit later), and these differences cause ClickHouse to perform extremely well for many of our needs: queries are fast, performance is predictable, and we’re able to filter and aggregate on more event attributes than we were able to before. Even more amazingly, we can do it with fewer machines and smaller disks due to the shockingly good compression that can be achieved with columnar data layouts.
-
-### Immutable data
-
-ClickHouse can make many of these performance improvements because data that has been written is largely considered to be [immutable](https://en.wikipedia.org/wiki/Immutable_object), or not subject to change (or even deleted). Immutability plays a large role in database design, especially with large volumes of data — if you’re able to posit that data is immutable, DML statements like `UPDATE` and `DELETE` are no longer necessary.
-
-If you’re just inserting data that never changes, the necessity for transactions is reduced (or removed completely), and a whole class of problems in database architecture goes away. This strategy works well for us — in general, we consider events that are sent to Sentry immutable once they have been processed. This decision is mostly a practical one: for example, the browser version that a user was using when they encountered an error is effectively “frozen in time” when that event occurs. If that user later upgrades their browser version, the event that we recorded earlier doesn’t need to be rewritten to account for whatever version they’re using now.
-
-### And not-so-immutable data
-
-But wait — while we do treat the event data that is sent to Sentry as immutable, the issues those events belong to can be deleted in Sentry, and those deletions should cause the events associated with those issues to be deleted as well. Similarly, while you can’t update the attributes of an event, you can modify its association with an issue through merging and unmerging. While these operations are infrequent, they are possible, and we needed to find a way to perform them in a database that wasn’t designed to support them.
-
-Unfortunately, all of the massive improvements for the common cases were also drawbacks for several of the uncommon cases that exist in Sentry — but uncommon doesn’t also mean unsupported. In the remainder of this field guide, we’ll explore how mutability affects database design and performance and how we deal with mutating data in a database architecture that was primarily designed for storing immutable data: in this case, specifically ClickHouse.
-
-## Mutating data v1: ClickHouse’s `ALTER TABLE`
-
-One of our first attempts leveraged ClickHouse’s [ALTER TABLE mutations](https://clickhouse.yandex/docs/en/query_language/alter/#alter-mutations), which are documented as “intended for heavy operations that change a lot of rows in a table.” On paper, this looked exactly like what we were looking for. However, we used the feature when it was initially released, which came with a non-trivial amount of bugs.
-
-Our favorite being [a bug](https://github.com/ClickHouse/ClickHouse/pull/2694) where the entire database was rewritten to alter a single row. Once the regression was fixed, we sought out to use `ALTER UPDATE` again, but to our dismay, we could only apply a single mutation at a time. Sentry users love to merge issues, and applying mutations one at a time meant constantly rewriting millions of rows to mutate thousands of rows. Even when mutations ran as fast as they could, they could not keep up with the request rate, and we would ultimately hit the high watermark for queued mutations.
-
-Without the ability to delete data directly — ClickHouse has no `DELETE` statement — we had to think about the problem from a different angle. If we can’t delete the data, could we at least overwrite its content and prevent it from being returned in future result sets? And, how could we do this all without ClickHouse having an `UPDATE` statement?
-
-### A quick aside: ClickHouse data storage
-
-First, it’s important to know a bit about how ClickHouse stores data on disk so that we can identify what kind of options we have at our disposal. ClickHouse provides a variety of table storage engines that can be used depending on the specific needs of the table they are backing. Of the different table engines provided by ClickHouse (and there are a lot), our favorites are members of [the MergeTree family](https://clickhouse.yandex/docs/en/operations/table_engines/mergetree/). `MergeTree` implementations are superficially similar to the [log-structured merge-tree](https://en.wikipedia.org/wiki/Log-structured_merge-tree) data structure (or LSM tree) used by a wide variety of data stores, such as the SSTable used by Cassandra.
-
-Like the LSM tree, data is stored in sorted order by primary key, making for efficient lookups by primary key and efficient range scans for ranges that share primary key components. When using the different variants of the `MergeTree` table engine family, each table has a defined `ORDER BY` clause, which also roughly equates to a primary key definition. One member of the `MergeTree` family is the `ReplacingMergeTree`, which supports a “row version” that is backed by an unsigned integer, date, or datetime column used to determine which version of a row should be preserved in the event of a primary key conflict.
-
-## Mutating data v2: deleting by replacing
-
-For the table that stores Sentry events, we’ve chosen to use the `ReplacingMergeTree` engine. The example schema below has the same key structure as our data model for Sentry events, but elides many of the data fields for brevity:
-
-```sql
-CREATE TABLE events
-(
- event_id FixedString(32),
- project_id UInt64,
- group_id UInt64,
- timestamp DateTime,
- deleted UInt8 DEFAULT 0,
- primary_hash Nullable(FixedString(32)),
- data Nullable(String)
-)
-ENGINE = ReplacingMergeTree(deleted)
-PARTITION BY toMonday(timestamp)
-ORDER BY (project_id, toStartOfDay(timestamp), cityHash64(toString(event_id)))
-
-Ok.
-
-0 rows in set. Elapsed: 0.079 sec.
-```
-
-```sql
-SELECT *
-FROM events
-
-┌─event_id─────────────────────────┬─project_id─┬─group_id─┬───────────timestamp─┬─deleted─┬─primary_hash─────────────────────┬─data─┐
-│ 00000000000000000000000000000000 │ 1 │ 1 │ 2019-10-30 00:00:00 │ 0 │ c4ca4238a0b923820dcc509a6f75849b │ data │
-└──────────────────────────────────┴────────────┴──────────┴─────────────────────┴─────────┴──────────────────────────────────┴──────┘
-
-1 rows in set. Elapsed: 0.012 sec.
-```
-
-Our table includes a deleted `UInt8 DEFAULT 0 column` that is used as the row version. When an event is deleted, we insert a new record with the same primary key as the existing row and the value of the `deleted` column set to `1` — essentially overwriting the original record with a [tombstone]().
-
-```sql
-INSERT INTO events (event_id, project_id, group_id, timestamp, primary_hash, data, deleted) VALUES ('00000000000000000000000000000000', 1, 1, '2019-10-30 00:00:00', 'c4ca4238a0b923820dcc509a6f75849b', '', 1);
-
-Ok.
-
-1 rows in set. Elapsed: 0.027 sec.
-```
-
-In Snuba, we refer to these rows as “replacements,” since they cause the old row to be replaced by the new row. To ensure that these replacement markers are not included in result sets for future queries, we automatically append the `deleted = 0` expression to the `WHERE` clause of all queries executed against this table. At this point, we should expect to have only one row (our deletion tombstone) in the table:
-
-```sql
-SELECT *
-FROM events
-
-┌─event_id─────────────────────────┬─project_id─┬─group_id─┬───────────timestamp─┬─deleted─┬─primary_hash─────────────────────┬─data─┐
-│ 00000000000000000000000000000000 │ 1 │ 1 │ 2019-10-30 00:00:00 │ 0 │ c4ca4238a0b923820dcc509a6f75849b │ data │
-└──────────────────────────────────┴────────────┴──────────┴─────────────────────┴─────────┴──────────────────────────────────┴──────┘
-┌─event_id─────────────────────────┬─project_id─┬─group_id─┬───────────timestamp─┬─deleted─┬─primary_hash─────────────────────┬─data─┐
-│ 00000000000000000000000000000000 │ 1 │ 1 │ 2019-10-30 00:00:00 │ 1 │ c4ca4238a0b923820dcc509a6f75849b │ │
-└──────────────────────────────────┴────────────┴──────────┴─────────────────────┴─────────┴──────────────────────────────────┴──────┘
-
-2 rows in set. Elapsed: 0.011 sec.
-```
-
-Well… that’s not what we were looking for. Instead of the deleted event replacing the original event, now we have two events with the same primary key — one that isn’t deleted, and one that is. What’s going on?
-
-### How `MergeTree` works
-
-To know what is happening here, we have to dig in a little bit deeper into how the `MergeTree` works. The `MergeTree` design differs from the LSM tree in that a table is split into “partitions” that are defined in the schema definition, rather than levels based on the order that writes occurred.
-
-For example, a table containing time series data might be partitioned by hour, day, or week depending on the amount of data the table contains. Each of these partitions contains one or more data files on disk, which are called “data parts.” Each `INSERT` to a table creates a new data part with its contents for the partitions affected — ClickHouse favors large writes for this reason — and these data parts are later merged together with other parts within that partition during a process referred to as optimization. Optimization combines several smaller parts within a partition by unioning their contents together, sorting the combined contents by primary key, and replacing these smaller parts with the new, larger part.
-
-The different flavors of `MergeTree` differ primarily around how primary key conflicts are handled when they are encountered during optimization. Wait… primary key conflicts? In many database architectures, this our deletion query would have failed to execute due to the failure to maintain a unique constraint on the primary key, since we’re inserting new rows with the same primary key as rows that already exist. **In ClickHouse, there are no unique constraints, and `MergeTree`-backed tables can have duplicate primary keys**. `ReplacingMergeTree` does not replace rows on insertion, it replaces rows during optimization, and it makes no attempt to reconcile the state of all returned rows by default to ensure that they are in the latest state.
-
-Knowing how the storage model works highlights an issue with the naive replacements approach: for a period of time, **both the original and (potentially multiple) replacement rows may be visible** since two (or more) rows with the same primary key exist in different data parts. Only during optimization are the rows with duplicate primary keys merged, leaving only the replacements behind.
-
-### `FINAL`ly reducing potential inconsistencies
-
-One option to reduce this potential for inconsistency is to force the table to be optimized by explicitly issuing the [OPTIMIZE FINAL statement](https://clickhouse.yandex/docs/en/query_language/misc/#misc_operations-optimize). Optimization is a resource-intensive process that merges all physical parts in a logical partition into a singular part, reading and rewriting every single row in a partition — the nuclear option, basically. As an alternative to running a table optimization, ClickHouse provides the `FINAL` keyword, which can be added to the `FROM` clause to collapse all duplicates during query processing, giving you the same result that you would have otherwise received when running a query immediately following an `OPTIMIZE`. Running our previous query with the `FINAL` keyword gives us our expected result:
-
-```sql
-SELECT *
-FROM events
-FINAL
-
-┌─event_id─────────────────────────┬─project_id─┬─group_id─┬───────────timestamp─┬─deleted─┬─primary_hash─────────────────────┬─data─┐
-│ 00000000000000000000000000000000 │ 1 │ 1 │ 2019-10-30 00:00:00 │ 1 │ c4ca4238a0b923820dcc509a6f75849b │ │
-└──────────────────────────────────┴────────────┴──────────┴─────────────────────┴─────────┴──────────────────────────────────┴──────┘
-
-1 rows in set. Elapsed: 0.009 sec.
-```
-
-The drawback to using `FINAL` is that queries are executed slower than they would be otherwise — sometimes by a significant margin. To quote the [ClickHouse documentation](https://clickhouse.yandex/docs/en/query_language/select/#select-from), “when using `FINAL`, the query is processed more slowly. In most cases, you should avoid using `FINAL`.” To avoid using `FINAL`, we keep track of a set (in Redis) of recently deleted issues for each project. Whenever we execute a query for that project, the set of recently deleted issues is added to the `WHERE` clause, automatically excluding the data from consideration without the need for `FINAL`.
-
-We also limit the overall size of this exclusion set so that projects that would require filtering a large amount of recently deleted issues are instead switched to a query path that utilizes FINAL rather than maintaining an extremely large exclusion set. In addition, we run scheduled optimizations to keep up with row turnover, which allows us to set an upper time bound on how long an issue is maintained in the exclusion set after the deletion was initiated.
-
-### From `1` to `NULL`: bulk operations
-
-It’s not a common case in Sentry to delete individual events — in fact, there isn’t an API endpoint that will delete a single event independently. There is, however, an endpoint that provides the ability to delete an entire issue at once. This operation would be straightforward for PostgreSQL: you’d just issue a `DELETE FROM events WHERE group_id = %s` query. What about in ClickHouse, where `DELETE` isn’t a defined SQL statement? If a user requests that we delete all of the events in an issue, how can we do that without deleting each event individually?
-
-Luckily, ClickHouse allows us to insert the result of a SELECT query with [INSERT INTO … SELECT](https://clickhouse.yandex/docs/en/query_language/insert_into/#insert_query_insert-select) statements. By crafting a query that selects all of the rows that are to be deleted and returning a result set containing each row’s primary key, the `deleted` column set to `1`, and all other column values set to either `NULL` or their default value, we can delete large numbers of rows in a single statement.
-
-```sql
-INSERT INTO events (event_id, project_id, group_id, timestamp, primary_hash, data, deleted) SELECT
- event_id,
- project_id,
- group_id,
- timestamp,
- NULL,
- NULL,
- 1
-FROM events
-WHERE (project_id = 1) AND (group_id = 2)
-
-Ok.
-
-0 rows in set. Elapsed: 0.018 sec.
-```
-
-The same strategy applies beyond deletions to other types of updates, such as merging two issues together. When merging two issues together, we can construct a query that rewrites events from an issue (or set of issues) into the new target issue:
-
-```sql
-INSERT INTO events
-(event_id, project_id, group_id, timestamp, primary_hash, data)
-SELECT event_id, project_id, 1, timestamp, primary_hash, data
-FROM events
-WHERE project_id = 1 and group_id = 2;
-```
-
-We can also use the exclusion set strategy on subsequent `SELECT` queries, in this case, to avoid being required to add `FINAL` to each query for the affected project since the issues that had their events moved to another issue are essentially deleted during the merge.
-
-## Lesson learned: don’t take database features for granted
-
-Looking back, not having in-place mutations wasn’t something that we thoroughly considered when moving our event storage to a totally different architecture. `UPDATE` and `DELETE` queries are so commonly available in many database systems that it’s easy to take them — and all of the other features and niceties of more fully-featured database — for granted.
-
-Often, though, the most effective way to improve the performance of a system is to strip away anything that isn’t essential to it’s functioning, and sometimes not having all of the batteries included from the start forces you to be a little more creative to make up for those shortcomings. As it turns out, the more you know about how something (like a database) works, the more you can trick it into doing what you want.
-
-It might feel like we’re taking the rental car off-road some days… but hey, if you want to write a Field Guide, you first have to get through the field.
diff --git a/src/content/blog/how-to-refactor-and-not-break-things.md b/src/content/blog/how-to-refactor-and-not-break-things.md
deleted file mode 100644
index 15bd54d4..00000000
--- a/src/content/blog/how-to-refactor-and-not-break-things.md
+++ /dev/null
@@ -1,119 +0,0 @@
----
-title: "How to Refactor and Not Break Things"
-date: "2024-07-08"
-tags: ["python", "refactoring", "sdk"]
-draft: false
-summary: "How we completed a huge refactoring of a software used by thousands of developers without breaking things."
-images: ["../../assets/images/how-to-refactor-and-not-break-things/hero.jpg"]
-postLayout: PostLayout
-authors: ["antonpirker"]
----
-
-In our Python SDK, we completed a huge refactoring, and I want to write down how we pulled this off without breaking (almost) anything and how we managed to stay mostly backward compatible.
-
-## The Initial Situation
-
-When you add Sentry to your application, it'll instrument the app and runtime to collect useful debugging information at runtime. Depending on the frameworks or libraries you use, the data is collected at different stages of process execution and is sent to Sentry at a later stage, asynchronously.
-If your application consists of multiple services (like frontend, backend, some microservices, or worker processes consuming items from a queue) the Sentry SDK also propagates tracing information between those services. This makes it possible to link the data from all your services into one trace.
-
-To handle all this data the SDK uses a thing called the Hub. The Hub holds a stack of so-called Scopes. All this was specified in the [Unified API](https://develop.sentry.dev/sdk/unified-api/) a long time ago. Data is sent to Sentry as events. Before sending events to Sentry the data from the Scope(s) is applied to those events. The SDK needs to make sure that only data from the right Scopes is applied to not leak data for example from one thread into the events captured by another thread. Think of web requests each having its own `url` tag, or `user.id`.
-
-## What We Wanted to Achieve
-
-We wanted to make our API simpler. Remove unnecessary abstractions where possible and thus make it easier for us and for open-source contributors to write new integrations for our SDK. The Hub-based API is used in our integrations and by power users who do custom performance instrumentations. It can be hard to wrap your head around the concepts of Hubs and Scopes. We wanted to simplify this.
-
-## Why We Wanted to Do This
-
-When doing a big refactoring like this the WHY is very important. We do not want to refactor just for refactoring's sake.
-In recent years [OpenTelemetry](https://opentelemetry.io/) has become more popular and we wanted to make sure Sentry is compatible with OpenTelemetry.
-We discovered that with our current Hub implementation that was not the case, so we decided to remove the Hub and switch to have only Scopes and make them behave like Contexts in OpenTelementry. This makes Sentry 100% compatible with OpenTelemetry and paves the way for the future.
-
-## This Is a Huge Undertaking, We need a Plan!
-
-Some goals that we wanted to achieve with the refactoring:
-
-- Be as backward compatible as possible. If possible, our users should not need to change their custom Sentry code.
-- Avoid introducing additional overhead. Do not use more CPU and memory compared to before the refactoring.
-- Do not break things. Behavior must stay the same.
-- Do it in baby steps. Have only PRs of manageable size. (No one can review a PR with 120 files changed.)
-- Because this is a massive change, we decided to make it a major version update. Sentry SDK 2.0!
-
-## Phase I: Preparation
-
-There is a quote by [Kent Beck](https://twitter.com/kentbeck/status/250733358307500032):
-
-> Make the change easy, then make the easy change. (Warning, the first part might be hard)
-
-In this first phase, we moved existing functionality from the Hub into the Scope. We did several PRs that each moved one or two functions from the Hub into the Scope and changed the function in the Hub to call its counterpart in the Scope.
-
-This way we had a couple of smaller PRs that each dealt with one topic and were easy to review.
-
-After we moved all the functionality from the Hub into the Scope our extensive test suite was still all green, giving us confidence that we did not change any behavior. Because neither the top-level Sentry API nor the Hub-based API was changed this could be released in a minor version update.
-
-This concluded the preparation of our canvas. Time to make the change.
-
-## Phase II: New Scopes
-
-Phase I left us with a hollow shell of a Hub and all functionality in the Scope. We now refactored the Scope. This was the biggest part. We changed how the Scope was stored in memory (it's not on the Hub anymore but saved as a Python Context Variable). We also introduced three different flavors of the Scope for better encapsulation of data. If you want to dig into details read our [develop docs on the topic](https://develop.sentry.dev/sdk/hub_and_scope_refactoring/).
-
-After we updated the Scope, the old Hub-based API could still be used, but under the hood, the new Scopes-based API was called.
-
-Again, our test suite gave us confidence that the SDK still behaved the same as before.
-
-We did not release this phase. We wanted to use the new API ourselves first. To see how it feels, to check if we have missed something, or if we can improve the ergonomics. So we did Phase III.
-
-## Phase III: Use New Scopes Everywhere
-
-In the Python SDK, we have over 40 integrations for various web frameworks, databases, and other libraries. All of them still used the Hub-based API.
-
-We updated each integration from the old Hub-based API to the new Scope-based API in a separate PR. This was pretty straightforward and done in a couple of days.
-
-Doing this gave us insights into the look and feel of the new API. Because we now used the API in the same way as our power users would use it in the future. We found some things that we did not like and made some minor changes to the new API to make it more convenient to use.
-
-Here is an example of a shortcut we [added](https://github.com/getsentry/sentry-python/pull/2844/files):
-
-```python
-# Before
-from sentry_sdk.scope import Scope
-Scope.get_client().should_send_default_pii()
-
-# After
-from sentry_sdk.scope import should_send_default_pii
-should_send_default_pii()
-```
-
-Having one PR for each integration created [a lot of small PRs](https://github.com/getsentry/sentry-python/pulls?q=is%3Apr+is%3Aclosed+label%3A%22SDK+2.0%22+use+new+scopes) that were easy to review.
-
-After we had all PRs merged, we created a release candidate and released it on PyPI so everyone could give it a try and we could gather feedback.
-
-We updated our internal usage dashboards so we could see the adoption of the new release candidate.
-
-## Phase IV: Load Testing and Dogfooding
-
-We started to do load testing. We created a [sample application and ran load tests](https://github.com/getsentry/demo-flask-load-test) locally to check if the CPU and memory usage would change between the current SDK and the new 2.0 version. It did not. After the load tests, we were confident that we could use the release candidate on Sentry.io.
-
-[Sentry.io is a very big Python code base](https://github.com/getsentry/sentry) where we use all the advanced features of the SDK. It is a perfect candidate for dogfooding because it will uncover problems very soon.
-
-We first installed the new SDK on our Canary servers. We let it run for an hour and closely monitored CPU and memory. Everything looked good. Like with the local load tests, nothing spiked.
-
-Time for some proper dogfooding.
-
-We installed SDK 2.0 on **all** our servers. After running the new version for a week on Sentry.io we discovered that there was a problem in the Celery integration where the baggage header (used for propagating trace information) was handled incorrectly. [After fixing this](https://github.com/getsentry/sentry-python/pull/2993), we created a new release candidate and continued the dogfooding.
-
-All in all, we created 6 release candidates where we each time fixed some smaller problems or made improvements.
-
-## Finally: The Release
-
-We fixed all issues we uncovered during dogfooding, giving us enough confidence to move forward. All data collected looked good, and CPU and memory usage was the same as before we [released version 2.0](https://github.com/getsentry/sentry-python/releases/tag/2.0.0).
-
-On the first day, there was a [bug report](https://github.com/getsentry/sentry-python/issues/3021) from a user when using the SDK with the Starlette framework and Uvicorn as the server. We fixed the problem and released [2.0.1](https://github.com/getsentry/sentry-python/releases/tag/2.0.1) right away.
-
-There was [one problem with propagating trace information in Celery](https://github.com/getsentry/sentry-python/issues/3068) that was caused by this refactoring.
-
-We are now a couple of months after the 2.0 release and the bug tracker has been quiet, no other regressions connected to the refactoring were reported.
-
-Our internal usage dashboards show that our users (like a very popular music streaming app) are adopting the new major version and are sending hundreds of millions of events to Sentry.
-
-We are confident that the refactoring was a success and our SDK is now set up for the future. Making it easier to implement anything the future might bring.
-
-And where does the newborn SDK go from here? The net is vast and infinite.
diff --git a/src/content/blog/how-we-built-user-interaction-tracking-for-jetpack-compose.md b/src/content/blog/how-we-built-user-interaction-tracking-for-jetpack-compose.md
deleted file mode 100644
index e721b317..00000000
--- a/src/content/blog/how-we-built-user-interaction-tracking-for-jetpack-compose.md
+++ /dev/null
@@ -1,267 +0,0 @@
----
-title: "How we built user interaction tracking for Jetpack Compose"
-date: "2023-04-21"
-tags: ["Android", "Jetpack Compose", "Kotlin", "Mobile"]
-draft: false
-summary: "Knowing the user interactions which happened in your app right before it crashed is crucial context information for fixing errors. Tracking interactions like click and swipes manually can be tedious, so we at sentry looked into ways on how to do that automatically for your Jetpack Compose enabled Android app. Learn how you can intercept any touch event, how to determine Composable identifiers and ultimately how our sentry Android SDK ties it all together."
-images:
- [../../assets/images/how-we-built-user-interaction-tracking-for-jetpack-compose/compose_hero.jpg]
-postLayout: PostLayout
-canonicalUrl: https://proandroiddev.com/how-we-built-user-interaction-tracking-for-jetpack-compose-e3b1dd24f0ae
-authors: ["markushintersteiner"]
----
-
-Like you, we’ve been noticing the demonstrative shift to declarative programming for mobile UIs. Late last year, we explored this shift [on our blog](https://sentry.engineering/blog/mobile-the-future-is-declarative), noting that while React Native and Flutter were declarative from the start, Android and iOS have both released support through Jetpack Compose and SwiftUI, respectively. Earlier this year we officially launched support for Jetpack Compose in our [Java SDK](https://github.com/getsentry/sentry-java), and I [hosted an AMA](https://www.youtube.com/watch?v=e_BxVAapYBw) along with the rest of the team that built the integration. With such a prominent shift happening in mobile development, it’s imperative that not only developer tools keep up, but that we share our learned experiences so that we can continue to develop this space together.
-
-Continuing our series of articles that began with our [getting started guide](https://sentry.engineering/blog/getting-started-with-jetpack-compose) earlier this year, my team and I will share our experience building our Jetpack Compose integration, starting with the user interaction tracking feature. With our SDK being open source and being early to adopt Jetpack Compose support, we want to invite you to learn from our wins and mistakes as you ramp up for the future of declarative mobile development.
-
-## Jetpack Compose and Monitoring
-
-In the Jetpack Compose [getting started guide](https://sentry.engineering/blog/getting-started-with-jetpack-compose) that we released earlier this year, one of the tips was to use an error and performance monitoring tool like Sentry to reduce the learning curve and ensure that your app is bug-free. In this post, we detail how we implemented the user interaction tracking feature for Jetpack Compose, which is available as part of our Android SDK.
-
-
-
-The final outcome: Automatically turning clicks into breadcrumbs
-
-## Our Requirements for Declarative Programming Support
-
-[Our Android SDK](https://docs.sentry.io/platforms/android/) gives developers deep context, like device details, threading information, and screenshots, that makes it easier to investigate an issue. It also provides breadcrumbs of user interactions (clicks, scrolls, or swipes) to fully understand what led up to a crash. And, like all of our other SDKs, our Android SDK is designed to provide this valuable information out-of-the-box without cluttering your code with Sentry SDK calls.
-
-We had the following goals in mind when building our user interaction tracking feature for Jetpack Compose:
-
-1. Detect any clicks, swipes, or scrolls, globally
-2. Know which UI element a user interacted with
-3. Determine an identifier for the UI element and generate the corresponding breadcrumb
-4. Require minimal setup
-
-## Detecting clicks, scrolls, and swipes
-
-In Jetpack Compose UI, a click behavior is usually added via `Modifier.clickable`, where you provide a lambda expression as an argument. Scrolling and swiping work similarly. That’s a lot of API surface to cover and spread throughout the user’s code. So how could an SDK track all those calls without asking the developer to add any custom code to every invocation? The answer is some nifty combination of existing system callbacks:
-
-1. On Sentry SDK init, register a `ActivityLifecycleCallbacks` to get hold of the current visible `Activity`
-2. Retrieve the `Window` via `Activity.getWindow()`
-3. Set a `Window.Callback` using `window.setCallback()`
-
-Let’s dive a bit deeper into the [Window.Callback](https://developer.android.com/reference/android/view/Window.Callback) interface. It defines several methods, but the interesting one for us is `dispatchTouchEvent`. It allows you to intercept every motion event being dispatched to an `Activity`. This is quite powerful and the basis for many features. For example, the good old [Dialog](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Dialog.java;l=776;drc=a2e45f1ed1a3f74ca413f5d4ef815d50f7399c26) uses this callback to detect clicks outside the content to trigger dialog dismissals.
-
-What’s important to note here is that you can only set a single `Window.Callback`, thus it’s required to remember any previously set callback (e.g. by the system or other app code out of your control) and delegate all calls to it. This ensures any existing logic will still be executed, avoiding breaking any behaviour.
-
-```kotlin
-val previousCallback = window.getCallback() ?: EmptyCallback()
-val newCallback = SentryWindowCallback(previousCallback)
-window.setCallback(newCallback)
-
-class SentryWindowCallback(val delegate: Window.Callback) : Window.Callback {
- override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
- // our logic ...
-
- return delegate.dispatchTouchEvent(event)
- }
-}
-```
-
-## Locating and identifying widgets
-
-But this is only half of the job done, as we also want to know which widget the user has interacted with. For traditional Android XML layouts, this is rather easy:
-
-1. Iterate the View Hierarchy, and find a matching View given the touch coordinates
-2. Retrieve the numeric View ID via `view.getId()`
-3. Translate the ID back to its resource name to get a readable identifier
-
-```kotlin
-fun coordinatesWithinBounds(view: View, x: Float, y: Float): Boolean {
- view.getLocationOnScreen(coordinates)
- val vx = coordinates[0]
- val vy = coordinates[1]
-
- val w = view.width
- val h = view.height
-
- return !(x < vx || x > vx + w || y < vy || y > vy + h);
-}
-
-fun isViewTappable(view: View) {
- return view.isClickable() && view.getVisibility() == View.VISIBLE
-}
-
-val x = motionEvent.getX()
-val y = motionEvent.getY()
-
-if (coordinatesWithinBounds(view, x, y) && isViewTappable(view)) {
- val viewId = view.getId()
- return view.getContext()
- .getResources()?
- .getResourceEntryName(viewId); // e.g. button_login
-)
-```
-
-As Jetpack Compose UI is not using the Android System widgets, we can’t apply the same mechanism here. If you take a look at the Android layout hierarchy, all you get is one large `AndroidComposeView` which takes care of rendering your `@Composables` and acts as a bridge between the system and Jetpack Compose runtime.
-
-
-
-Left: Traditional Android Layout, Right: Jetpack Compose UI
-
-Our first approach was to use some Accessibility Services APIs to retrieve a description of an UI element at a specific location on the screen. The [official documentation about semantics](https://developer.android.com/jetpack/compose/semantics) provided a good starting point, and we quickly found ourselves digging into [AndroidComposeViewAccessibilityDelegateCompat](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt) to understand better how it works under the hood.
-
-```kotlin
-// From
-
-/**
- * Hit test the layout tree for semantics wrappers.
- * The return value is a virtual view id, or InvalidId if an embedded Android View was hit.
- */
-@OptIn(ExperimentalComposeUiApi::class)
-@VisibleForTesting
-internal fun hitTestSemanticsAt(x: Float, y: Float): Int
-```
-
-But after an early prototype, we quickly abandoned the idea as the potential performance overhead of having accessibility enabled didn’t justify the value generated. Since Compose UI elements are not part of the traditional Android View system, the Compose runtime needs to sync the “semantic tree” to the Android system accessibility service if the accessibility features are enabled. For example, any changes to the layout bounds are synced every 100ms.
-
-```kotlin
-// From
-
-/**
- * This suspend function loops for the entire lifetime of the Compose instance: it consumes
- * recent layout changes and sends events to the accessibility framework in batches separated
- * by a 100ms delay.
- */
-suspend fun boundsUpdatesEventLoop() {
- // ...
-}
-```
-
-We also had little control over what the API returned, e.g., the widget descriptions were localized, making it unsuitable for our use case.
-
-## Diving into Compose internals
-
-So it was time to examine how Compose works under the hood closely.
-
-Unlike the traditional Android View system, Jetpack Compose builds the View Hierarchy for you. Your `@Composable` code “emits” all required information to build up its internal hierarchy of nodes. For Android, the tree consists of two different node types: Either `LayoutNode` (e.g. a `Box`) or `VNode` (used for Vector drawables).
-
-The before-mentioned `AndroidComposeView` implements the `androidx.compose.ui.node.Owner` interface, which itself provides a root of type `LayoutNode`.
-
-Unfortunately, some of these APIs are marked as internal and thus can’t be used from an outside module, as it will produce a Kotlin compiler error. We didn’t want to resort to using reflection to workaround this, so we devised another little trick: If you’re accessing the APIs via Java, you’ll get away with a compiler warning. 🙂 Granted, this is far from ideal, but it gives us some compile-time safety and lets us quickly discover breaking changes in combination with a newer version of Jetpack Compose runtime. On top of that, reflection would not have worked for obfuscated builds, as any `Class.forName()` calls during runtime wouldn’t work with renamed Compose runtime classes.
-
-After settling on the Java workaround, we quickly encountered another issue when adding Java sources to our existing sentry-compose Kotlin multiplatform module. The build fails if you try to mix Java into an [Kotlin Multiplatform Mobile (KMM)](https://kotlinlang.org/lp/mobile/) enabled Android library. This is a [known issue](https://youtrack.jetbrains.com/issue/KT-30878), and as a temporary workaround, we created a separate JVM module called sentry-compose-helper which contains all relevant Java code.
-
-Similar to a `View`, a `LayoutNode` also provides some APIs to retrieve its location and bounds on the screen. `LayoutNode.getCoordinates()` provides coordinates that can be fed into L`ayoutCoordinates.positionInWindow()`, which then returns an `Offset`.
-
-```kotlin
-// From:
-/**
- * The position of this layout relative to the window.
- */
-fun LayoutCoordinates.positionInWindow(): Offset
-
-You probably used `Offset` before, but did you know it’s actually a `Long` in a fancy costume? 🤡 `x` and `y` are just packed into the first and last 32 bits. This Kotlin feature is called [Inline Classes](https://kotlinlang.org/docs/inline-classes.html), and it’s a powerful trick to improve runtime performance while still providing the convenience and type safety of classes.
-
-@Immutable
-@kotlin.jvm.JvmInline
-value class Offset internal constructor(internal val packedValue: Long) {
- @Stable
- val x: Float
- get() // ...
-
- @Stable
- val y: Float
- get() // ...
-}
-
-Since we’re accessing the Compose API in Java, we had to manually extract x and y components from the Offset.
-
-private static boolean layoutNodeBoundsContain(@NotNull LayoutNode node, final float x, final float y) {
- final int nodeHeight = node.getHeight();
- final int nodeWidth = node.getWidth();
-
- // positionInWindow() returns an Offset in Kotlin
- // if accessed in Java, you'll get a long!
- final long nodePosition = LayoutCoordinatesKt.positionInWindow(node.getCoordinates());
-
- final int nodeX = (int) Float.intBitsToFloat((int) (nodePosition >> 32));
- final int nodeY = (int) Float.intBitsToFloat((int) (nodePosition));
-
- return x >= nodeX && x <= (nodeX + nodeWidth) && y >= nodeY && y <= (nodeY + nodeHeight);
-}
-```
-
-## Identifying Composables
-
-Retrieving a suitable identifier for a `LayoutNode` wasn’t straightforward either. Our first approach was to access the `sourceInformation`. When the Compose Compiler plugin processes your `@Composable` functions, it adds `sourceInformation` to your method body. This can then later get picked up by Compose tooling to e.g. link the Layout Inspector with your source code.
-
-To illustrate this a bit better, let’s define the simplest possible `@Composable` function:
-
-```kotlin
-@Composable
-fun EmptyComposable() {
-
-}
-```
-
-Now let’s compile this code and check how the Compose Compiler plugin enriches the function body:
-
-```java
-import androidx.compose.runtime.Composer;
-import androidx.compose.runtime.ComposerKt;
-import androidx.compose.runtime.ScopeUpdateScope;
-import kotlin.Metadata;
-
-public final class EmptyComposableKt {
- public static final void EmptyComposable(Composer $composer, int $changed) {
- Composer $composer2 = $composer.startRestartGroup(103603534);
- ComposerKt.sourceInformation($composer2, "C(EmptyComposable):EmptyComposable.kt#llk8wg");
- if ($changed != 0 || !$composer2.getSkipping()) {
- if (ComposerKt.isTraceInProgress()) {
- ComposerKt.traceEventStart(103603534, $changed, -1, "com.example.EmptyComposable (EmptyComposable.kt:5)");
- }
- if (ComposerKt.isTraceInProgress()) {
- ComposerKt.traceEventEnd();
- }
- } else {
- $composer2.skipToGroupEnd();
- }
- ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
- if (endRestartGroup == null) {
- return;
- }
- endRestartGroup.updateScope(new EmptyComposableKt$EmptyComposable$1($changed));
- }
-}
-```
-
-Let’s focus on the `ComposerKt.sourceInformation()` call: The second argument is a String, containing information about the function name and the source file. Unfortunately, `sourceInformation` isn’t necessarily available in obfuscated release builds, thus, we also can’t take advantage of that.
-
-After some more research, we stumbled upon the built-in `Modifier.testTag(“)` method, which is commonly used for writing UI tests. Turns out this is part of the accessibility semantics we already looked into earlier!
-
-At this point, it was little to no surprise to see that those semantics are being modeled as `Modifiers` under the hood (`Modifiers` are like a secret ingredient, making Jetpack Compose so powerful!). Since `Modifiers` are directly attached to a `LayoutNode`, we can simply iterate over them and look for a suitable one.
-
-```kotlin
-fun retrieveTestTag(node: LayoutNode) : String? {
- for (modifier in node.modifiers) {
- if (modifier is SemanticsModifier) {
- val testTag: String? = modifier
- .semanticsConfiguration
- .getOrNull(SemanticsProperties.TestTag)
-
- if (testTag != null) {
- return testTag
- }
- }
- }
- return null
-}
-```
-
-## Wrapping it up
-
-Having finished the last piece of the puzzle, it was time to wrap it up, cover some edge cases and ship the final product. Jetpack Compose user interactions are now available, starting with the `6.10.0` version of our Android SDK.
-
-Currently, the feature is still opt-in, so it needs to be enabled via `AndroidManifest.xml`:
-
-```xml
-
-
-
-
-```
-
-But after enabling it, it just works. Granted, it still requires you to provide a `Modifier.testTag(…)`, but that should already exist if you’re writing UI tests. 😉 [Check out our docs to get started](https://docs.sentry.io/platforms/android/configuration/integrations/jetpack-compose/)!
diff --git a/src/content/blog/how-we-fixed-incorrect-codecov-bundle-size-reporting.md b/src/content/blog/how-we-fixed-incorrect-codecov-bundle-size-reporting.md
deleted file mode 100644
index e2053125..00000000
--- a/src/content/blog/how-we-fixed-incorrect-codecov-bundle-size-reporting.md
+++ /dev/null
@@ -1,101 +0,0 @@
----
-title: "How we fixed incorrect Codecov bundle size reporting"
-date: "2024-08-19"
-tags:
- ["javascript", "bundler", "bundle analysis", "codecov", "git", "github", "ci", "github actions"]
-draft: false
-summary: "How we resolved incorrect Codecov bundle size reporting when using GitHub Actions."
-images:
- [
- "../../assets/images/how-we-fixed-incorrect-codecov-bundle-size-reporting/codecov-classroom-hero.jpg",
- ]
-postLayout: PostLayout
-canonicalUrl:
-authors: ["nicholasdeschenes"]
----
-
-## What is Bundle Analysis?
-
-Bundle analysis is a new product offering from Codecov. This product consists of a set of bundler plugins that users can choose from for their specific bundler or meta-framework. Once a plugin is installed and configured in the respective configuration file, the plugins will run when the application is being bundled. During the bundling process the plugins will collect and organize the assets, chunks, and modules for your bundle into a stats file and upload these stats to Codecov. With these stats we enable developers to gain insights into their JavaScript bundles, such as overall bundle size, problematic assets, etc. Bundle analysis relies heavily on your Git workflows similar to any other Codecov product. We closely replicate your Git tree to give you insights at major points in the development lifecycle such as commits and pull requests.
-
-## How Codecov Follows Along with your Git Flows
-
-To replicate your Git tree when bundle reports are uploaded to Codecov they are sent along with the corresponding commit SHA so we can create the commit and grab more details about it from GitHub such as the parent commit, the author, commit message, etc. Typically we end up creating commits in Codecov after users have opened a PR and ran their CI. Which at the same time as opening your pull request Codecov receives a webhook from GitHub and will create a pull request entry in our database storing the relevant information such as the base and head commit SHAs.
-
-
-
-Your repository often contains information that is unrelated to tests and JavaScript bundles like CI configuration or documentation. All this info requires commits to be updated properly within your repository. Typically these changes will result in your CI not running and in turn not running your tests or building your bundle and sending a report to Codecov. So, how can we compare the difference between commits and give you the correct information when a parent commit doesn’t exist in Codecov? To accomplish this, we will “walk” up the Git branch until we find a parent commit that has a valid report to compare against and store that information in our pull request entry. This enables us to correctly compare the information between your latest changes and the most recent reports that were uploaded to Codecov.
-
-In typical CI environments this works pretty well, as they expose the correct Git information (typically) as environment variables, which we grab inside the bundler plugins while they’re executing and pass along with the bundle stats information. However, when running GitHub Action workflows you’re required to run `actions/checkout`. This action uses Git to copy your repository into the action runner, however during the steps of this action it creates a merge commit between the head commit of your feature branch and the head commit of the branch you’re looking to merge to. A problem arises when running this action and trying to get the correct commit SHAs. Because the action creates this new merge commit it creates a detached commit that does not belong to any branch, and its “parent” commit is not the same as your feature branches head commit
-
-> This is an action that checks out your repository onto the runner, allowing you to run scripts or other actions against your code (such as build and test tools). You should use the checkout action any time your workflow will use the repository's code.
-> ~ [GitHub Docs](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions)
-
-## Base and Head Commit SHA Issues with GitHub Actions
-
-Here on line 73 you can see highlighted in the image this checkout to the merge-commit occurring:
-
-
-
-```shell
-HEAD is now at 3c5c246 Merge 46950b9de2b3ae7e946cc446709d5b42c88416b9 into 8ce5086a68a1827d04d3d6b6b07b7962d4b2a72e
-```
-
-This flow creates a two big problems for Codecov when we try and determine the correct commit SHA. This new commit that is being created does not belong to any branch inside the repository, which is a limitation for Codecov as we expect commits to belong to a valid branch and to have a single parent rather than two. Secondly, this commit takes the head of your branch and merges it with the head of the comparison branch, not the commit that you originally branched off of. This results in an incorrect comparison because there are changes in the branch you’re merging into that do not appear in the base commit for your branch.
-
-## Solving Incorrect Head Commit SHA’s
-
-Let’s tackle the first problem that arises here, the creation of a new commit SHA. When this action happens it sets the GITHUB_SHA to this new detached commit SHA. Because this commit only exists in this detached state it doesn’t have any useful information to it and we don’t want to use it to create the commit inside of Codecov. To address this issue in the bundler plugins, we utilize the @actions/github package, this enables us to grab details from the GitHub Action context payload. We first need to check and see if the action is running in a pull request. We can do this by checking the context event name to see if it matches that of a pull request. Now that we know we’re running in a pull request, we can grab the pull request details from the context payload which includes information about the head commit and the correct SHA.
-
-```javascript
-import * as GitHub from "@actions/github";
-
-// ...
-
-function findCommitSHA() {
- let commit = envs?.GITHUB_SHA;
- const context = GitHub.context;
- if (["pull_request", " pull_request_target"].includes(context.eventName)) {
- const payload = context.payload as PullRequestEvent;
- commit = payload.pull_request.head.sha;
- }
-
- return commit;
-}
-```
-
-We have tackled the issue of avoiding the creation of detached commits in Codecov, and can associate the correct bundle stats information with the commit where the changes were made. A new problem now arises in repositories that have a large number of contributors, making large amounts of commits to your default branch, in turn moving quickly. With these fast moving repositories, PRs that are opened have their changes compared to the latest commit on the branch being merged into instead of your branches base commit.
-
-## A Second Problem Arises
-
-The second problem that occurs is one that’s a little more confusing to get your head around, and it took us some time to figure out what was actually going on. During the action, it checkouts to a new commit that is based off of your branch head commit and the current head commit of the comparison branch. To better explain the entire process that happens here is a graphic showing what is actually going on through-out the entire process:
-
-
-
-Lets break down what's happening in this graphic:
-
-1. User checks out to their new feature branch from their repositories default branch that they will later attempt to merge their changes into.
-2. User has implemented their new feature and commits their changes, and pushes the changes to GitHub.
-3. User opens up a new PR on GitHub triggering their CI to run.
-4. Running GitHub Action workflow after PR is opened
- 1. `actions/checkout` step is ran checking out their changes and creating a merge commit based off of the users feature branch commit, and the current head commit of the branch that they have targeted with their PR.
- 2. Bundler plugin runs during application build and grabs the relevant Git information such as branch name, head commit SHA, etc. It then takes that information and uploads it alongside the bundle stats data to Codecov.
-5. Compare passed head commit with its direct parent commit.
-
-So with this graphic and how Codecov compares against the parent commit you may be able to see the problem that we were facing. The problem arises when the action checkouts to the detached head (green circle) and uses the latest comparison branch head commit (yellow circle) as the base, however because Codecov sees the detached head commit (green circle) actually as the branch head commit (blue circle) it compares it against the original base commit (red circle) instead of the now correct base commit (yellow circle).
-
-## Solving Incorrect Base Commit SHA’s
-
-So what is the solution here? Well, we are already grabbing the correct head commit SHA, why can we not just grab the correct comparison base commit SHA? It turns out, we can for the bundler plugins. It is not a giant leap away from how we grab the current correct head commit, and it was a fairly small implementation change.
-
-
-
-1. User checks out to their new feature branch from their repositories default branch that they will later attempt to merge their changes into.
-2. User has implemented their new feature and commits their changes, and pushes the changes to GitHub.
-3. User opens up a new PR on GitHub triggering their CI to run.
-4. Running GitHub Action workflow after PR is opened
- 1. `actions/checkout` step is ran checking out their changes and creating a merge commit based off of the users feature branch commit, and the current head commit of the branch that they have targeted with their PR.
- 2. Bundler plugin runs during application build and grabs the relevant Git information such as branch name, head commit SHA, compare commit SHA, etc. It then takes that information and uploads it alongside the bundle stats data to Codecov.
-5. Compare branch passed head commit against passed comparison commit.
-
-When it comes to the internal side of Codecov there are a few more complications that we run into and how we handle comparisons, as I mentioned earlier, we have a table of pull requests and in this table we store the base, head, and compare to commit SHA, however we cannot override this compare to SHA as it is the correct one for coverage comparisons. Okay, so can we just add a new field to the table? Well, we could, however the pulls table isn’t exactly “small” with millions of rows, which would require us to lock that table until the operation has completed. Instead we have decided to take a slightly different approach that is tailored to our bundle analysis setup. When users upload a stats report for the first time, it is sent directly to GCP and our “worker” will pick up the file and process it, with the resulting information being stored inside of a SQLite DB, any other bundle stats that are uploaded for a given commit are than merged into that same SQLite DB. It will be inside this SQLite DB where we will store the correct comparison SHA for bundle analysis, and as a precaution we will fallback to the pull request compare to commit SHA.
diff --git a/src/content/blog/how-we-grew-sentrys-monthly-active-users-by-rethinking-invitations.md b/src/content/blog/how-we-grew-sentrys-monthly-active-users-by-rethinking-invitations.md
deleted file mode 100644
index acd723b4..00000000
--- a/src/content/blog/how-we-grew-sentrys-monthly-active-users-by-rethinking-invitations.md
+++ /dev/null
@@ -1,157 +0,0 @@
----
-title: "How we grew Sentry's monthly active users by rethinking invitations"
-date: "2020-02-12"
-tags: ["building sentry", "growth"]
-draft: false
-summary: "At its core, Sentry is a tool that alerts you to defects in your production software. But it does more than blast stack traces into your inbox: Sentry provides powerful workflows to help your team determine root cause, triage issues to your team, and keep tabs on ongoing concerns with comments and notifications. At the end of 2019, the Growth team made it our mission to make it easier for our users to invite their teammates to join them on Sentry. Our theory: improving the user experience of inviting users, as well as democratizing the process to include all team members would lead to a significant increase in team-wide adoption. (Narrator: it did.)"
-images:
- [../../assets/images/how-we-grew-sentrys-monthly-active-users-by-rethinking-invitations/hero.jpg]
-postLayout: PostLayout
-canonicalUrl: https://blog.sentry.io/2020/02/12/how-we-grew-sentrys-monthly-active-users-by-rethinking-invitations/
-authors: ["benvinegar", "evanpurkhiser"]
----
-
-At its core, Sentry is a tool that alerts you to defects in your production software. But it does more than blast stack traces into your inbox: Sentry provides powerful workflows to help your team determine root cause, [triage issues](https://blog.sentry.io/2019/02/07/sentry-workflow-triage) to your team, and keep tabs on ongoing concerns with comments and notifications.
-
-These collaborative features can help you resolve problems with your software quickly. But the keyword here is **collaborative**; without your full team having access to Sentry, you may find yourself quickly becoming overwhelmed with an endless backlog of issues and no one to help.
-
-At the end of 2019, the Growth team made it our mission to make it easier for our users to invite their teammates to join them on Sentry. To achieve this, we tackled three distinct areas:
-
-1. Surfacing the ability to invite users contextually
-2. Expanding Sentry’s permission model to allow more _types of users_ to send invitations
-3. Allowing external users to request access themselves
-
-Our theory: improving the user experience of inviting users, as well as democratizing the process to include all team members would lead to a significant increase in team-wide adoption. (_Narrator: it did._)
-
-## The status quo
-
-Before we get deep into what we changed and how it impacted the bottom line, let’s quickly revisit how user invitations worked: the _Add Member to Organization_ page you see below.
-
-
-
-This full-page experience, tucked away deep in Sentry’s account settings, had a number of issues:
-
-1. Since this is a full page, reaching it meant you would be taken out of context of whatever you were doing previously
-2. Its location deep in our navigation hierarchy meant discoverability was poor
-3. It’s unclear you can actually invite multiple people at once (you can!)
-4. When inviting multiple users, you could only assign the group to the same role and collection of teams
-
-Interestingly, to improve discoverability, we had previously introduced a number of “quick links” to reach this page more easily. But these links were scattered around the application, and didn’t appear contextually when users signaled intent to invite members.
-
-
-
-These user experience and discovery challenges felt like obvious starting places. But instead of just settling for a new “improved” form, we decided to rethink the entire experience from the ground up.
-
-## The new member invitation modal
-
-It’s probably not a surprise that our first instinct was to convert this page into a modal – one that manages to squeeze all of the capabilities shown earlier into a smaller, more concise experience. It actually does one better: the modal is clearer in indicating that you can invite multiple users, and allows you to set unique permissions for each invitee.
-
-
-
-While modals are sometimes overused in web applications, we believed this approach would solve our key discoverability and navigation concerns: it can be shown contextually without leaving the current page, and completing the form returns you to what you were doing.
-
-To that point, we additionally introduced buttons to launch this _Invite New Members_ modal contextually throughout Sentry:
-
-- Viewing an issue and notice a [suspect commit](https://docs.sentry.io/workflow/releases/?platform=node#after-associating-commits) made by a coworker? If they’re not already part of your Sentry organization, you can now invite them then and there.
-- Trying to assign an issue to a team member, but don’t see their name in the assignee list? You now have the option to invite them right in the dropdown.
-- Creating a new team? Now you can invite new members directly to that team as you go.
-
-
-
-_Example contextual link that launches the Invite New Members modal_
-
-## Democratizing invitations
-
-As we began rolling out our new _Invite New Members_ experience across the application, we came to a sobering realization: only roughly **half** of Sentry users could actually use our new modal. That’s because historically, only those with Owner or Manager-level permissions could invite other team members. Combined, users with these permissions accounted for less than 50% of active users.
-
-
-
-Restricting the ability to add new users to account administrators is pretty common practice for software tools, and Sentry is no exception. When an employee onboards on a new team, it’s common to see an exchange like this:
-
-> Alice: Oh awesome, we use Sentry. Can you add me to the organization? Bob: Ah, I can’t invite you. Maybe ask Jen?
-
-In a perfect world, one of your administrators is tracked down, and they manually add the new teammate to the account. But sometimes that person is unknown, or is on a vacation, or maybe it takes them days or weeks or even months.
-
-We began asking ourselves: what if we could unlock team members to fast-track this whole process and invite members themselves? This led to our next major change: updating our permission model to allow for **members to request to invite other members**.
-
-
-
-Now, when non-administrators open up the Invite New Members modal, it changes contextually to become a “request to invite” rather than a direct invitation. Hitting “send” kicks off an email to all organization administrators, who are prompted to approve any outstanding requests.
-
-
-
-_Organization owners and managers can see pending invitation requests._
-
-At this point, we had built what we thought was a fantastic new user experience and we just doubled the number of users who could take advantage of it. But this exercise of opening up user invitations got us thinking: what if there was an even further source of untapped users we weren’t reaching?
-
-## Removing the middleman entirely
-
-In the last section, we highlighted a scenario where one teammate asks another teammate for access to Sentry. And because of our recent changes, users can request to invite their teammates themselves instead of having to track down and ask their account administrator.
-
-But this scenario still has a gatekeeping element: the new teammate has to ask another teammate for access. What if the new teammate spots an alert from Sentry in Slack stemming from their recent changes, and no one’s around to grant them access? Unfortunately, they’d land on an authentication wall that would prevent them from going any further.
-
-
-
-This begged the question: was there an untapped source of potential users we weren’t reaching by restricting invitations _only_ to active Sentry users? What if new users didn’t have to ask anyone at all?
-
-So, to keep this party going, we dug in and additionally made it possible for **external users to request to join an organization**.
-
-
-
-_The “Request to Join” button has been added to the organization login page, allowing_
-Now when a user lands on an Organization’s login page, they have the option to “Request to join”, which asks for the user’s name and email address. Once they hit send, the organization owners are sent an email that prompts them to approve the join request. Just in case, there’s also a call-to-action to disable the feature entirely for their organization.
-
-
-
-Having developed a new contextual invitation modal and a pair of invite-friendly permission changes, we were feeling confident that these changes were going to have a strong impact on user behavior. It was time now to put our money where our code was, and verify that all this hard work actually moved the needle.
-
-## A/B testing the impact
-
-Our standard procedure for validating the efficacy of product changes is through A/B testing (also known as split testing). This means instrumenting our application code to serve different experiences to segments of users over the same time period, and comparing the results. This step takes extra effort, but it’s worth it – the alternative is to settle for a before-and-after snapshot of data, which is too easily impacted by external factors like seasonality or marketing pushes.
-
-ℹ️ _To learn more about how we perform A/B testing at Sentry, please see [this earlier blog post](https://blog.sentry.io/2019/05/09/easy-ab-testing-with-planout)._
-
-It’s easy to get carried away with A/B testing, and having so many permutations that you don’t have enough data to be statistically significant. So, to simplify things, we decided that all treatments would get the new modal experience, and our “treatment” groups would focus on the new permission changes.
-
-This left us with 4 distinct treatments that we rolled out to 4 equally-sized customer segments:
-
-1. **Baseline** (new modal only)
-2. **Request to Invite** (user invites another user)
-3. **Request to Join** (external user requests to join)
-4. **Both** Request to Invite and Request to Join are enabled together
-
-Our main criteria for determining success: which of these treatments would result in **an increase in accepted invitations** (and thus new users)?
-
-
-
-_% of accepted invitations relative to baseline_
-
-After 30 days, the results became clear (not to mention, statistically significant):
-
-- **11%** more users accepted invitations in the _Request to Invite_ treatment vs. the baseline
-- **9%** more users accepted invitations in the _Request to Join_ treatment vs. the baseline
-- **21%** more users accepted invitations who had both _Request to Invite_ and treatments enabled
-
-It’s probably not a surprise that allowing a wider set of users to invite team members resulted in more users inviting team members. It’s also probably not a surprise that enabling both feature sets at the same time was even better (given that they complement each other)!
-
-## Turning users into active users
-
-Having more users join your platform is great, but what’s the point if those users never actually use the product? To be truly confident our changes were successful, we need to additionally make sure that invitees in the treatment groups were actually using Sentry.
-
-Internally, we view “active users” as those who meaningfully interact with the product within a 30 day period (vs. just signing in) – for example, viewing or triaging an issue. In the baseline treatment, users became active after accepting an invitation at a rate of 71.3%. In the remaining treatment groups, that number ranged from 72.7% to 78.4%.
-
-
-
-_% of users who become active in Sentry within 30 days after accepting an invitation_
-
-The first interesting observation is that despite us making it easier to invite users, overall acceptance rate of invites _actually went up_. This bucks the trend of conversion optimization projects improving metrics in one part of the funnel at the expense of other metrics further down the funnel. This can be attributed to more _qualified users_ being invited in the new flows.
-
-The second observation is that the _Request to Invite_ variant significantly outperformed the other variants. Our guess is that this is because members inviting their teammates to collaborate is one of the best ways to acquire new engaged users.
-
-## Rolling it out and final thoughts
-
-Having accumulated enough data to feel confident in our results (and to publish the data above), we recently turned off our A/B tests and rolled out these changes to all organizations. If you’re a Sentry user on _any_ organization, you’ll now find these new contextual invite links and invite-friendly permission changes available to use.
-
-What began as a simple project to improve our invitation UX expanded to include meaningful permission changes that significantly improved the rate at which new users both join and use Sentry. We didn’t deploy any trickery or “growth hacks” to achieve these results; we just made the product better by getting out of the way of the user and letting them solve their own problems. As product developers, it doesn’t get more satisfying than that.
-
-_This project was a combined effort from our multi-disciplinary Growth team: Evan Purkhiser (Software Engineer), Megan Heskett (Software Engineer), John Manhart (Designer), AJ Jindal (Head of Growth), and Adhiraj Somani (Product Manager)._
diff --git a/src/content/blog/how-we-improved-performance-score-accuracy.md b/src/content/blog/how-we-improved-performance-score-accuracy.md
deleted file mode 100644
index ba85299b..00000000
--- a/src/content/blog/how-we-improved-performance-score-accuracy.md
+++ /dev/null
@@ -1,65 +0,0 @@
----
-title: "How we improved Performance Score accuracy"
-date: "2024-01-17"
-tags: ["javascript", "performance", "web"]
-draft: false
-summary: "We're making updates to how Performance Scores are calculated in the Web Vitals module, which will bring them closer to what your users experience."
-images: ["../../assets/images/how-we-improved-performance-score-accuracy/hero.jpg"]
-postLayout: PostLayout
-canonicalUrl: performance-score-improvements
-authors: ["edwardgou"]
----
-
-Last year in November, we released the [Web Vitals](https://docs.sentry.io/product/performance/web-vitals/) module for Sentry's Performance product. Aside from helping users monitor and improve their Web Vitals, this new module also introduced [Sentry Performance Scores](https://docs.sentry.io/product/performance/web-vitals/#performance-score) for all web browser applications. As a quick reminder, [Performance Scores](https://docs.sentry.io/product/performance/web-vitals/#performance-score) are used to condense multiple Web Vitals and their respective thresholds into an overall score from 0-100 that rates an app's perceived performance, based on real user data for Web Vitals.
-
-Today, we are making updates to how Performance Scores are calculated. These updates will improve the accuracy of these scores by bringing them closer to what users experience on your web apps. To understand how these changes will impact your Performance Scores, it's useful to understand how they used to be calculated. Here's a brief overview:
-
-1. Your users visit your webpage which generate pageload transactions. These transactions contain [Web Vitals](https://docs.sentry.io/product/performance/web-vitals/) (LCP, FCP, FID, TTFB, CLS) and get sent to Sentry.
-2. In the Sentry Web Vitals module, each web vital is aggregated to get the p75 value.
-3. The p75 value of each Web Vital is then put through a non-linear function which generates individual Web Vital component scores from 0 to 100. - It's not necessary to know, but for those curious, we use a [Complementary Log-Normal CDF](https://www.desmos.com/calculator/ejhjazajbd) to calculate component scores:
-
- $$C\left(x\right)=\frac{1}{2}(1-E_{rf}(\frac{\ln x-\mu}{\sqrt{2}\sigma}))$$
-
-4. Each component score is then statically weighted and summed to produce an overall Performance Score out of 100 for your web app:
-
- $$S_{total}=S_{lcp} \times 30 + S_{fid} \times 30 + S_{cls} \times 15 + S_{fcp} \times 15 + S_{ttfb} \times 10$$
-
-You can find out more about how these are calculated [in our documentation](https://docs.sentry.io/product/performance/web-vitals/#performance-score).
-
-While the above approach gives some of the signal that we're looking for, there are some drawbacks that affect how accurately our Performance Scores reflect what users experience:
-
-- Performance Scores can be skewed based on outlier pageloads if the Web Vitals are extreme enough.
- 1. Consider a scenario where we have three pageload transactions, with the following [LCP](https://docs.sentry.io/product/performance/web-vitals/#largest-contentful-paint-lcp) values and thresholds:
- ```
- 300ms -> Good
- 400ms -> Good
- 8000ms -> Bad
- ```
- 2. Aggregating the LCP values above would get a p75 of 4200ms.
-
- $$p75(300ms,400ms,8000ms)=4200ms$$
-
- 3. Which equates to an LCP score of **26** in our Complementary Log-Normal CDF.
-
- $$C_{lcp}(4200ms)=0.26$$
-
- 4. Although two of the samples in the scenario have very performant LCP values, the one outlier sample overly skews the LCP score negatively.
-
-- Some Web Vitals may be reported less frequently, depending on [browser support](https://docs.sentry.io/product/performance/web-vitals/#browser-support). These Web Vitals end up overrepresented in Performance Scores because we use static weights.
- - Consider a scenario where our web app has a single LCP sample from Chrome and 100 pageload samples without LCP from Safari (because Safari does not support LCP). This causes LCP to be overrepresented in our performance score because LCP makes up a static 30% weight of our overall Performance Score despite having a sample size of one.
-
-Ideally, our Performance Scores should feel true to what users experience on our web app. If our web app is fast and responsive, our Performance Scores should positively reflect that. Additionally, Performance Scores should show a consistent relationship when drilling down from web app, to pages, to pageloads.
-
-Rather than calculating scores on aggregate Web Vitals across your app, a better approach would be to calculate the score on individual pageloads at the point of their collection. The first advantage is if any Web Vitals are missing on a pageload, we can dynamically adjust weights to exclude those missing Web Vitals. The second advantage is that we can now average pageload scores to come up with an overall score across our entire web app without skewing our scores due to outlier pageloads. This is because individual pageload scores are bounded between 0 and 100, which mitigates the impact of any outliers.
-
-To demonstrate, if we use the previous example of three pageload transactions, we get the following approximate LCP score values
-
-```
-300ms -> 99
-400ms -> 99
-8000ms -> 7
-```
-
-Averaging the scores above gets us a score of **68**. Compared to the previous score of **26**, this new score of **68** lines up much better with what we would expect for 2 fast pageloads and 1 slow pageload. This is how we will be calculating Performance Scores going forward. As a result of improved Performance Score accuracy, we’ll also see more accurate [Opportunity Scores](https://docs.sentry.io/product/performance/web-vitals/#opportunity) as well.
-
-Due to the updates we're deploying to Performance Score calculations, users may see a change in their data the next time they visit the Web Vitals module. For most users, this may mean an increase in their Performance Score due to the correction in outliers no longer skewing scores negatively.
diff --git a/src/content/blog/how-we-made-javascript-stack-traces-awesome.md b/src/content/blog/how-we-made-javascript-stack-traces-awesome.md
deleted file mode 100644
index a45cf8d7..00000000
--- a/src/content/blog/how-we-made-javascript-stack-traces-awesome.md
+++ /dev/null
@@ -1,203 +0,0 @@
----
-title: "How We Made JavaScript Stack Traces Awesome"
-date: "2022-11-30"
-tags: ["javascript", "errors", "stack traces"]
-draft: false
-summary: Sentry helps every developer diagnose, fix, and optimize the performance of their code, and we need to deliver high quality stack traces in order to do so. In this blog post, we want to explain why source maps are insufficient for solving this problem, the challenges we faced, and how we eventually pulled it off by parsing JavaScript.
-images: ["../../assets/images/how-we-made-javascript-stack-traces-awesome/sourcemaps.png"]
-postLayout: PostLayout
-canonicalUrl: In this blog post, we want to explain why source maps are insufficient for solving this problem, the challenges we faced, and how we eventually pulled it off by parsing JavaScript.
-authors: ["arminronacher", "arpadborsos", "kamilogorek"]
----
-
-
-
-Sentry helps every developer diagnose, fix, and optimize the performance of their code, and we need to deliver high quality stack traces in order to do so.
-
-You might have noticed a significant improvement in Sentry JavaScript stack traces recently. In this blog post, we want to explain why source maps are insufficient for solving this problem, the challenges we faced, and how we eventually pulled it off by parsing JavaScript.
-
-As a JavaScript developer, you are probably all too familiar with minified stack traces. Even with source maps present, browsers are typically unable to show you something readable in the console:
-
-
-
-You would think that showing the correct function names in a stack trace would be the primary use case of source maps. Ironically source maps are almost entirely useless for this purpose.
-
-In the past, you might have seen some Sentry stack traces, even with source maps in use, where the function name was a misleading `apply`, `call`, `fn`, or some completely random unexpected function name. We long wanted to improve this.
-
-Say hello to JavaScript Source Scopes.
-
-## Setting the Stage
-
-When working with a JavaScript engine, Sentry is mainly concerned with stack traces, most of which are typically minified, which means the line, column, or displayed function name must be corrected. It will also be highly unstable between different releases and transpiler runs. That’s no fun, and so for many years, we have supported source maps to deobfuscate stack traces. If you want to go deep into the gnarly issues with source maps and the troubles we faced with them [you might also want to read this post](https://blog.sentry.io/2019/07/16/building-sentry-source-maps-and-their-problems/).
-
-## Old Implementation
-
-For years, our solution to the problem relied solely on [rust-sourcemap](https://github.com/getsentry/rust-sourcemap), a Rust crate we wrote to speed up the processing of source maps, which works great in most cases.
-
-We use source maps to ask three questions: where is this in the original file? What’s the original function name? What’s the surrounding function?
-
-The first and third questions are easy to answer, source maps are quite good at that. But the function names are incorrect. That’s because when we ask a source map about location information (say line 1337, column 42), it points at some token, but not the one that declares the function. Remember the questionable stack trace from the browser console earlier? That’s because modern browser stack trace rendering does not try to answer that question at all. Source maps are really just a way to map token to token, and what we care about is understanding the surrounding scope information, which is frustratingly missing in source maps. Another way to think about this is that we don’t need to know what token is called when the function call happened, but the name of the function the token is contained in.
-
-Take this trivial example:
-
-```js
-function thisLovesToCrash() {
- callTheCrashingThing();
-}
-
-function callTheCrashingThing() {
- throw new Error("kaputt");
-}
-```
-
-After minification, it turns into an absolute mess. For simplicity reasons, let’s assume the compiler renames the first function, `thisLovesToCrash`, to `a` and the second function, `callTheCrashingThing`, to `b`.
-
-```js
-function a() {
- b();
-}
-function b() {
- throw new Error("kaputt");
-}
-```
-
-The line and column information will point to neither of those functions when we look at the stack trace. Instead, in the first case, it will point to the token that calls into the function (`b` aka `callTheCrashingThing`), and in the second case, it might point to the `new` keyword, for instance, or maybe the constructor invocation of `Error`. If we were to consult the source map, we would not be able to retrieve the function name.
-
-We had various heuristics about this, but it always fell short. In the past, we applied two heuristics: backward scanning for function declarations and caller naming.
-
-Backwards scanning meant that when we were, for instance, placed on the `throw` keyword, we would take the minified function name that was sent to us from the stack trace (`b`), and then scan backwards until we find the minified function name preceded by a token called `function`. That way, we can sometimes find the correct function name.
-
-
-
-Obviously, this does not work for ES6 method declarations of anonymous functions, class methods, or object property methods.
-
-Caller naming is an approach where we take a gamble that the caller of our function refers to us by the same function name. In that case, we go one frame up, land in the function `a`, and then look at the current token information (`b`) and translate that token back to `callTheCrashingThing`. As with backwards scanning, this won’t work for reassigned function names. Many times the function would just be called `apply`, `call` or something like `fn`.
-
-
-
-Things like anonymous callbacks, class methods, and object literal properties are notoriously wrong. It can give you some information sometimes, but usually, it’s not enough for the system to correctly classify and group exceptions that contain such information together.
-
-On top of that, it sometimes takes effort to tell where the call comes from without looking at the source code itself.
-
-## Our Improved Approach: JavaScript Parsing
-
-In recent months, we worked hard to bring new ideas and solutions to the table. The idea is not entirely new, we have considered [doing this in the past](https://blog.sentry.io/2019/07/16/building-sentry-source-maps-and-their-problems/). The idea was to parse the original code to reconstruct source scopes, but actually getting a parsable source for all the languages transpiling to JavaScript was always considered a problem with a very large scope.
-
-We did, however, reconsider this approach recently, and this work became something we call [SourceMapCache](https://github.com/getsentry/symbolic/tree/master/symbolic-sourcemapcache). It lives as a part of our Rust `symbolic` crate, which handles all sorts of debugging formats.
-
-It is built on top of our [js-source-scopes](https://github.com/getsentry/js-source-scopes) crate, where the nitty gritty bits and pieces live, providing the functionality for extracting and processing scope information from JavaScript source files and resolving that scope via source maps. The change in approach is that, rather than reconstructing the scopes from the source files, we are **consulting the transplied minified JavaScript files instead**.
-
-We have always encouraged developers to upload their minified files as build artifacts and not just their source files. In fact, even if you do not upload them, we always try to fetch the minified files in addition, as there is vital information in them, such as the location of the correct source map. As a result, the minified source is usually available to us. We now parse the minified JavaScript sources to reconstruct scope information. This means that we can not only ask about the name of a specific thing living on a line/column pair, but also understand all its surroundings. That allows us to make better decisions on what the function should be called and what information to present to the end user.
-
-### How Does That Work in Detail?
-
-The short answer is that we are parsing JS files and know what functions they contain and their names. We detect all scopes in the minified file, scan associate a name and then map it back to the original names via the source maps. For the example above this is what this looks like:
-
-
-
-But this gets a lot more complex with multiple scopes. Let’s look at a snipped of (minified, but pretty-printed) JS with a couple of functions in it:
-
-```js
-function a() {}
-const b = () => {};
-a.prototype.b = () => {};
-class A {
- a() {}
- get b() {}
-}
-const c = {
- a() {},
- b: () => {},
-};
-```
-
-In this example, only the function `a` has an explicit name. However, we can also infer the names of the other functions from the context in which they are defined. For example, the function in `const b = () => {};` does not have an explicit name itself. But it is assigned to a variable named `b`, so we will use that.
-
-We do similar things if a function is assigned to `a.prototype.b`, for example. For functions that are members of classes or object expressions, we also consider the name of the “parent”. A similar pattern arises there, as class expressions do not have to have an explicit name either. In which case, we infer that from its context, and so on.
-
-
-
-Now that we have collected all the functions and their names from the minified JavaScript source we again can go back and map these tokens to original names.
-
-`a.prototype.b` can be deconstructed into `a` and `b` individually. The same also applies to the name `prototype`, but we ignore that, for now. For each of these individual components, we do a lookup in the provided source map. And if the source map is properly constructed, we will get a `name` back. With this, we can map `a` to `MyClass` and `b` to `myMethod`, or `MyClass.prototype.myMethod`, putting all the components back together again.
-
-### But What if Functions truly Have no Name?
-
-In some contexts, it is not obvious how to name functions. For example, for anonymous callback functions.
-
-```js
-doStuff(() => {});
-```
-
-Couldn’t we just infer `doStuff` as the name in this case? Well, it’s not that easy, unfortunately. What if we had a lot more of these?
-
-```js
-doMoreStuff(
- () => {},
- () => {},
- () => {},
-);
-```
-
-Because of these ambiguities, we decided not to infer a name for functions being passed as parameters to other function calls.
-
-However, we do have another trick up our sleeves. When the function itself does not have a name, we can still look at the complete stack trace.
-
-```js
-const a = b(() => {});
-
-const result = a();
-// ^ we call the function here
-```
-
-While the function itself does not have a name, we consider the place it is called from. The `a()` in this case, which is the caller frame.
-
-In this case, we will look up the `a` in the source map, similar to how we looked up the individual name components above.
-
-And the source map will tell us that the function being called was named `setMyState`.
-
-This example highlights how this heuristic is a good fit for React Hooks.
-
-Sometimes though, a function may be called as `someFn.apply(...)`. In which case, we would infer `apply`. Not great, not terrible.
-
-There can also be situations where neither the function itself, nor the way it is being called, reveals anything about its name. That might be the case for IIFEs (immediately invoked function expressions).
-
-## And What Are the Improvements?
-
-So let’s look at some of the improvements in practice. Here is a piece of code (in a class called `Activity`) that previously, after minification, would produce a completely incorrect function name:
-
-```js
-get imageWidth(): number | undefined {
- return this.controller.getCurrentImage()?.width;
-}
-```
-
-Sentry in the past would most likely report a completely nonsensical function name here. The reason is that these (either if transpiled / minified to ES6 or ES5) would not match our “search backwards to function declaration” heuristic since it does not really define a function. Also, the fallback to the caller often results in incorrect names because, in this particular case, it’s an attribute access and not a function call on the caller side. With the new changes, Sentry will report `get Activity.imageWidth` here as function name, which tells you not only the class and property name, but also that it’s a property!
-
-Generally, the ability to see class names in addition to method names makes reading stack traces a much nicer experience. Have a look at the before and after from one of our errors in our own frontend project:
-
-
-
-**Old Stack Trace**: notice the incorrect function `apply` and missing function name information for the bottom frame.
-
-
-
-**Improved Stack Trace**: all methods now also show their method class names and `apply` now has the correct method name. The anonymous function now also is clearly visible.
-
-Not only did completely incorrect function names such as “apply” disappear, but we can now quickly see the class’s name. While in our own code, you could in many cases guess the name of the class, as we try to have only one class per file, it is still much easier to read with the additional contextual information. It also makes it easier to spot standalone functions such as the “errorHandler” or “callback” in this example now that they can be clearly distinguished from methods.
-
-The following stack trace shows this even better. Notice how “apply” turned into “unlisten.current”, and the “\_legacyStoreHookUpdate” now tells us that it’s bound to “window”. React components here also show an interesting effect. When callbacks are bound to complex structures, we would end up with things like `