Shai Malul published this on Zencity Engineering on 24 February 2025. It is first-party. The scope is frontend logging across micro-frontends. It is not a written uninstall of every Sentry project, and there is no cost number.
Zencity adopted micro-frontends more than four years before the post, so teams could ship their own components. That left logging split. Each of 10-plus micro-frontends initialized its own Sentry. Errors were fragmented across repos, the call shapes drifted, frontend and backend errors were hard to line up, and configuration was a pile of copies.
They wanted Datadog so frontend and backend observability lived in one place. The Sentry → Datadog playbook assumes you are moving issues, traces, and monitors. This post moves the call sites, and leaves the provider behind an event.
Why the Datadog SDK did not sit inside each app
The first attempt loaded the Datadog RUM SDK through module federation. Three problems killed it. The SDK still had to ship in each micro-frontend. The apps became coupled to that package. And Datadog’s own micro-frontend approach did not match how Zencity loads assets. He does not list the asset rule that failed. He only says it did not fit.
The shell is the host. It owns routing and authentication, and it loads the other apps. They put the provider there, and made the micro-frontends emit browser events instead of talking to Datadog. A micro-frontend does not import the RUM SDK. It does not know which vendor is listening. The shell subscribes, stamps the event, and forwards it. Swap the vendor later and the shell is the only code that changes. He names Sentry, LogRocket, or a custom sink as things that could sit in that spot. Datadog RUM is what they actually initialized.
beforeSend is where a remote error gets its identity back. If the event context has remote.service and remote.version, those overwrite event.service and event.version. The shell’s own package name and version are stored beside them as shellService and shellVersion, and isRemoteError is set true. The hook returns true so the event is kept. Without that rewrite, every remote error would look like it came from the shell.
React errors use a ZCDErrorBoundary. onError calls logger.error with the package name, the error, and a component name. His example component is UserDashboard. Errors outside React go through a window error listener in the shell, registered with capture: true. The handler looks at ErrorEvent for filename, line, and column. He also says it covers network errors and DOM exceptions. The snippet in the post is abbreviated, so I would not treat it as the full handler.
Two hundred calls, five repos, one shape
The new pipe was useless until the old calls matched it. He estimates about 200 logging calls in each of 5-plus repositories. The shapes had drifted:
logger.error(error)
logger.error('Message', { context })
logger.error(error, 'Message')
logger.error(`Error occurred: ${error}`)
The target shape is a message, then the error, then context. logger.error('Meaningful message', error, context).
They used JSCodeshift, which walks an abstract syntax tree and rewrites the call. The transformer had to pull a message out of whatever was there, keep the context object, and not destroy a template literal. He says they kept an abstraction so old call sites still worked during the rollout, and that adoption was gradual across micro-frontends. There is no list of which repo moved first, and no week when the last Sentry init was deleted.
The design choice that matters is the event boundary. Module federation would have shared a SDK and still left each app knowing about Datadog. A browser event lets the shell be the only place that imports it, which is also why a later vendor swap stays small. The gap in the write-up is the old data. Nothing here says they exported Sentry issues, mapped releases, or ran the two tools on the same traffic. If you need that overlap, this post does not document it.