<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nico Krijnen</title>
    <description>The latest articles on DEV Community by Nico Krijnen (@realworldarchitect).</description>
    <link>https://dev.to/realworldarchitect</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3638558%2Fd60225da-8ae5-445e-b815-59692c6703fc.jpg</url>
      <title>DEV Community: Nico Krijnen</title>
      <link>https://dev.to/realworldarchitect</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/realworldarchitect"/>
    <language>en</language>
    <item>
      <title>MCP Apps - Finally a UI paradigm that speaks the language of intent</title>
      <dc:creator>Nico Krijnen</dc:creator>
      <pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/realworldarchitect/mcp-apps-finally-a-ui-paradigm-that-speaks-the-language-of-intent-1dkj</link>
      <guid>https://dev.to/realworldarchitect/mcp-apps-finally-a-ui-paradigm-that-speaks-the-language-of-intent-1dkj</guid>
      <description>&lt;p&gt;Earlier this month I spoke at the &lt;a href="https://aiconference.nl/" rel="noopener noreferrer"&gt;Dutch AI Conference&lt;/a&gt;. After my own talk, I attended a talk by &lt;a href="https://glennreyes.com" rel="noopener noreferrer"&gt;Glenn Reyes&lt;/a&gt; about &lt;a href="https://glennreyes.com/talks/mcp-ui-and-the-future-of-interactive-conversations" rel="noopener noreferrer"&gt;MCP-UI&lt;/a&gt;. While listening, something profound struck me, something that I think not many others may have realized. So I figured this insight is worth writing down and sharing.&lt;/p&gt;

&lt;p&gt;Glenn argued that due to the text-based nature of our interactions with LLMs and agents, we are throwing away decades of UX expertise. &lt;strong&gt;MCP-UI&lt;/strong&gt; has been an attempt to change that, allowing interactive visual components to be provided through MCP (Model Context Protocol), an open standard that gives AI assistants the ability to connect to external tools and take real action, not just generate text. Development of MCP-UI went quite fast and currently, interactive HTML-based interfaces are an official part of the &lt;strong&gt;MCP Apps&lt;/strong&gt; standard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqneixjyhwx1taqpp7r2w.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqneixjyhwx1taqpp7r2w.webp" alt="Glenn Reyes at Dutch AI Conference 2026" width="720" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/extensions/apps" rel="noopener noreferrer"&gt;MCP Apps&lt;/a&gt; solve the &lt;em&gt;how do we make agent UIs better&lt;/em&gt; problem. But sitting in that audience, a different question grabbed me: how do you &lt;em&gt;design&lt;/em&gt; what those agent interactions should be? Because building a beautiful MCP App that solves the wrong problem is still solving the wrong problem.&lt;/p&gt;

&lt;p&gt;That question has been answered before. Not by the AI community, but by a different group of practitioners who’ve spent the past decade perfecting collaborative techniques for exactly this: understanding complex behavior, modeling intent, and building shared language between the people who know the domain and the people who write the code. These two worlds have barely met. So let’s see if we can bring these communities together!&lt;/p&gt;

&lt;h2&gt;
  
  
  Troublesome state
&lt;/h2&gt;

&lt;p&gt;UIs have always been built around &lt;em&gt;state&lt;/em&gt;. Lists, forms, search bars, toggles, modals, loading spinners, all of these require state to be managed. Many frameworks have been built in attempts to tame state complexity (Redux, MobX, signals), but the fundamental model stays the same: UI reacts to data, components reflect state. Even with these frameworks, UI code is still hard to reason about, hard to test, and far away from the language of the business.&lt;/p&gt;

&lt;h2&gt;
  
  
  Language of the business
&lt;/h2&gt;

&lt;p&gt;What do I mean with “language of the business”, and why is that important? Developers (and their coding agents) write the code. To build the right thing, they need to understand the &lt;em&gt;what&lt;/em&gt; and &lt;em&gt;why&lt;/em&gt; behind what they are building or changing. Otherwise they solve the wrong problem…&lt;/p&gt;

&lt;p&gt;The understanding that is needed typically lives with non-technical people: the users of the system or the people who know how things really work. They don’t speak your technical language and they shouldn’t have to. What you need is to build deep understanding by using a shared non-technical language. More on that later…&lt;/p&gt;

&lt;h2&gt;
  
  
  Intent
&lt;/h2&gt;

&lt;p&gt;What struck me during Glenn’s talk was that MCP-UI and MCP Apps don’t manage state in the usual way that we’ve been doing that for frontend components. Instead, the primitive is the &lt;em&gt;message&lt;/em&gt; or &lt;em&gt;tool call&lt;/em&gt;, not the component. The UI becomes a surface for expressing goals, not navigating menus. Instead of “fill form, submit, handle error state”, the interaction is “user states intent, agent invokes tool, result surfaces”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmegahm9jobp1pxq4n8y.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmegahm9jobp1pxq4n8y.webp" alt="State-based UI vs intent-based interaction" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In MCP Apps, every meaningful interaction is a named action with context. Things like: “SearchProducts”, “PlaceOrder”, “SummarizeDocument”. Sounds familiar? That’s because it’s very close to the &lt;em&gt;commands&lt;/em&gt; used in EventStorming. Think of an MCP tool call like agent’s way of issuing a command to the outside world. A command is simply a request to do something. It captures what someone wants to happen in a name and with the information needed to do it. Commands model intent directly, they express “I want this to happen” rather than “set this field to that value”. They carry &lt;em&gt;meaning&lt;/em&gt;, not mechanics.&lt;/p&gt;

&lt;p&gt;A command like “ApproveExpenseReport” says &lt;em&gt;why&lt;/em&gt; something needs to happen, not just &lt;em&gt;how&lt;/em&gt; the data changes. When modeled correctly, commands speaks the language of the business, not the language of the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0qkakts4qybpmrrnjm7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0qkakts4qybpmrrnjm7.webp" alt="MCP tool calls map to commands in domain modeling" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Intent-based AI interactions
&lt;/h2&gt;

&lt;p&gt;Let’s start with a concrete example. Imagine you’re building an MCP App for a warehouse operations team. Order pickers need to collect items from shelves and get them to the loading dock. A picker says “plan my picks for the next batch” and the agent needs to respond with an optimized walking route through the warehouse. You can’t describe a spatial route in text. This domain &lt;em&gt;needs&lt;/em&gt; a visual interface. So how do you design it?&lt;/p&gt;

&lt;p&gt;Instead of the usual: “let the dev team dream up how this will work”, let’s do some collaborative modeling and build some deeper understanding.&lt;/p&gt;

&lt;p&gt;You get the warehouse team into the room and run an EventStorming session. You start by asking: “What are all the things that &lt;em&gt;happen&lt;/em&gt; during a pick run?” Sticky notes go up: &lt;code&gt;PickOrderReceived&lt;/code&gt;, &lt;code&gt;RouteOptimized&lt;/code&gt;, &lt;code&gt;ItemPicked&lt;/code&gt;, &lt;code&gt;CartFull&lt;/code&gt;, &lt;code&gt;BatchCompleted&lt;/code&gt;. Standard stuff, roughly what you expected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5u1irp0m5mn371m1sftm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5u1irp0m5mn371m1sftm.webp" alt="Warehouse pick process EventStorming board" width="720" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the warehouse supervisor adds one: &lt;code&gt;ObstructionReported&lt;/code&gt;. You ask what that means. Turns out aisle blockages happen &lt;em&gt;every day&lt;/em&gt;: pallet spills, restocking crews. When a planned route is blocked, pickers currently find out by walking into the obstruction and rerouting on the fly, or by shouting across the floor. This was nowhere in the requirements.&lt;/p&gt;

&lt;p&gt;You keep going. &lt;code&gt;ColdChainTimerStarted&lt;/code&gt; appears: frozen items have a 12-minute window from cold storage to the insulated shipping container. &lt;code&gt;HazmatSegregationViolationDetected&lt;/code&gt;: cleaning chemicals and food products can never share a cart. The process is far richer than anyone expected. Commands pop up: &lt;code&gt;PlanPickRoute&lt;/code&gt;, &lt;code&gt;PickItem&lt;/code&gt;, &lt;code&gt;ReportObstruction&lt;/code&gt;, &lt;code&gt;SplitCart&lt;/code&gt;, &lt;code&gt;SubstituteItem&lt;/code&gt;. Each maps directly to an MCP tool definition, named in the language of the warehouse team.&lt;/p&gt;

&lt;p&gt;Then something interesting happens. When the supervisor explains how obstruction rerouting works, sticky notes aren’t enough. She grabs a marker and draws the floor plan on the whiteboard next to the event timeline. Aisles, shelving sections, cold storage tucked in the back corner, the loading dock. She marks where today’s restocking is blocking aisle 6 and draws the detour path a picker would take. Someone grabs colored markers: orange zones for hazmat storage, a blue snowflake near cold storage, red X’s for blockages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwg01zf32a4v1oke2s7qz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwg01zf32a4v1oke2s7qz.webp" alt="Warehouse floor plan evolving from whiteboard sketch to MCP App UI" width="720" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You zoom in on the tricky parts with Example Mapping. You pick &lt;code&gt;PlanPickRoute&lt;/code&gt; and explore the rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rule: cold chain&lt;/strong&gt; : “Given a batch that includes frozen items, when the route is planned, then cold storage items must be picked last to stay within the 12-minute window.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule: hazmat segregation&lt;/strong&gt; : “Given a batch with both cleaning chemicals and food products, when the route is planned, then the cart must be split and items picked in separate runs.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge case:&lt;/strong&gt; “What if the only route to cold storage is blocked?” The supervisor pauses. “Good question. I think… the picker skips the cold items and comes back for them after the obstruction clears? We’ve never actually decided that.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1vd774kpfvniv06u4bl6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1vd774kpfvniv06u4bl6.webp" alt="Example Mapping for PlanPickRoute MCP tool" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sketch on the wall becomes the MCP App: a floor plan with aisles, an optimized route line, obstructions in red, color-coded item categories. The events and commands from the timeline map to tool definitions. The visual the supervisor drew to explain her world is the visual the picker will use every day. The modeling session didn’t just uncover &lt;em&gt;what&lt;/em&gt; the interactions should be, it produced the UI too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which techniques did you say?
&lt;/h2&gt;

&lt;p&gt;Understanding how things work and building systems that fit into that is something we’ve been doing for a long time already, so it’s no surprise that there are plenty of tools that we can use to do that effectively. I think the following 4 have a an exceptionally good use for designing AI agent interactions, especially when using MCP and MCP Apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  EventStorming
&lt;/h3&gt;

&lt;p&gt;This collaborative modeling technique helps you understand system behavior. It maps business processes as a storyline of domain events, triggered by commands from actors. This is exactly how an AI-driven UI interaction flows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbhuwh2aw2f3b802ubis.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbhuwh2aw2f3b802ubis.webp" alt="EventStorming timeline mapped to MCP interaction flow" width="720" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://www.eventstorming.com/" rel="noopener noreferrer"&gt;EventStorming&lt;/a&gt;, the initial focus is only on events: facts that have happened. That is not by accident. When you think primarily about command and actions, it is easy to fall into the trap of modeling &lt;strong&gt;how&lt;/strong&gt; things work. They model intention: things may still end up different than intended. The big benefit of starting with events is that they force you to focus on desired outcomes, not on how you get there. That gives you the freedom to find better ways to do things, to find a simpler command trigger for an event, still arriving at the same outcome. Both EventStorming and Event Modeling also include sketching UI interactions directly on the wall: wireframes, screens, visual cues that show what the user sees at each step. For MCP Apps, those sketches naturally become the design for custom visual components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain Storytelling
&lt;/h3&gt;

&lt;p&gt;More suitable for interview style, &lt;a href="https://domainstorytelling.org/" rel="noopener noreferrer"&gt;domain storytelling&lt;/a&gt; captures how actors work together to achieve goals through concrete work stories. These stories help you understand how the domain actually works. Again, this is a very natural fit for mapping the intent-flows of AI agent and MCP interactions, told in the language of the domain. When you use this technique, you draw an easy to understand visual story of what the person you’re talking to is explaining. The key here is the quick feedback and validation. If you misunderstood something, they will instantly point out the mistake in your drawing or point out edge cases not yet covered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fib1s53e4bfqx45b43pn1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fib1s53e4bfqx45b43pn1.webp" alt="Domain Storytelling" width="720" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Event Modeling
&lt;/h3&gt;

&lt;p&gt;Very similar to EventStorming, also modeling the intent (commands), the facts (events) and the views (read models). This feels like an almost perfect conceptual match for MCP tool calls, conversation history and context windows.&lt;a href="https://eventmodeling.org/" rel="noopener noreferrer"&gt;Event Modeling&lt;/a&gt; organizes parts of the flow as vertical &lt;em&gt;slices&lt;/em&gt;, each slice a complete flow from intent to state change to updated view. The key being that each slice is an isolated piece of behavior that can be implemented as one unit of work. This makes estimation and planning a breeze. As each slice is isolated, it can also be implemented in parallel with other slices without waiting for the rest of the system to be done. Productivity-wise, this is rather interesting now that we can put many AI agents to work in parallel to each build a piece of a system. Slices also map nicely to how MCP Apps work, each slice essentially one tool interaction: what the user asks for, what happens and what they see as a result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntdup4rahweaoim4dxrg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntdup4rahweaoim4dxrg.webp" alt="Event Modeling" width="720" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example Mapping
&lt;/h3&gt;

&lt;p&gt;The more concrete you can make it, the easier it is to spot edge cases or incorrect assumption. &lt;a href="https://cucumber.io/blog/bdd/example-mapping-introduction/" rel="noopener noreferrer"&gt;Example Mapping&lt;/a&gt; is an excellent tool to quickly explore the rules around a specific command or intent: given &lt;em&gt;this&lt;/em&gt; situation, when &lt;em&gt;this&lt;/em&gt; happens, then &lt;em&gt;this&lt;/em&gt; should be the outcome. Perfect to capture exactly what an MCP tool should do and how it should respond in different scenarios.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0zu7h6t80bbnvpkwd31k.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0zu7h6t80bbnvpkwd31k.webp" alt="Example Mapping" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nowadays, you can take the raw output of an example mapping session, give it to your coding assistant and turn it into a test-suite. And with good behavioral tests and the right architecture guidelines, your coding agent will have a breeze creating the implementation for you too.&lt;/p&gt;




&lt;p&gt;What I find most interesting is how all of these techniques are not just similar to how MCP tools work, conceptually they map almost one-to-one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real superpower
&lt;/h2&gt;

&lt;p&gt;All these are not just modeling tools, they are &lt;strong&gt;collaborative discovery&lt;/strong&gt; tools. They work so well at uncovering complex behavior because they put &lt;strong&gt;experts and developers in the same room&lt;/strong&gt; : working side by side without any intermediaries. This direct contact creates shared understanding &lt;em&gt;before&lt;/em&gt; a single line of code is written. It also creates that insight very quickly and much deeper than with written specs. Result: high focus during implementation, with very few questions or points of confusion. And when the inevitable unforeseen edge case emerges, the personal connections that developers have built up with the experts mean that they know exactly who to call.&lt;/p&gt;

&lt;p&gt;Contrast that with the traditional approach: &lt;strong&gt;BA → Specs → PO → Story → Tasks → Dev/AI → Code&lt;/strong&gt;. Where every handoff is a translation loss: the telephone-game effect in action. By the time the requirements reach the implementor, they are abstract, ambiguous, full of assumptions and often wrong in ways nobody notices until late. The typical result is expensive rework, long feedback cycles and clarity arriving at the worst possible moment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa0iyalcnuf49slsxrqoa.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa0iyalcnuf49slsxrqoa.webp" alt="Translation loss through handoffs vs direct collaborative discovery" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The language aspect matters even more for our interaction with AI agents, where the “UI” is largely &lt;em&gt;language&lt;/em&gt;. If you get the intent vocabulary wrong, the whole thing feels broken to it’s users. This focus on precise, shared language has its roots in &lt;a href="https://www.domainlanguage.com/ddd/" rel="noopener noreferrer"&gt;Domain-Driven Design&lt;/a&gt; and is now more important than ever. Getting the vocabulary right is no longer just a modeling concern, it’s a usability concern. I guess that deserves a blog of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this all means for you
&lt;/h2&gt;

&lt;p&gt;AI agents are becoming part of how we build, design and use software. Intent-based interaction through MCP fit naturally to command and event based design tools. Collaborative modeling techniques like EventStorming, Domain Storytelling, Event Modeling, and Example Mapping have been perfected over the past decade for exactly this kind of problem: understanding what a system should do by working directly with the people who know. If you want to build MCP Apps that actually solve the right problems, &lt;strong&gt;start by getting the right people in the room&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’d like a hand getting started, whether it’s facilitating a good modeling session or designing MCP tool interactions that speak your domain’s language, &lt;a href="https://realworldarchitect.dev/contact/" rel="noopener noreferrer"&gt;reach out&lt;/a&gt;. I’d love to help.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
      <category>ui</category>
    </item>
    <item>
      <title>Traceable Tests</title>
      <dc:creator>Nico Krijnen</dc:creator>
      <pubDate>Fri, 27 Jan 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/realworldarchitect/traceable-tests-b6c</link>
      <guid>https://dev.to/realworldarchitect/traceable-tests-b6c</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.luminis.eu/blog/traceable-tests/" rel="noopener noreferrer"&gt;Luminis&lt;/a&gt; on 27 Jan 2023.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Have you ever spent hours or days trying to figure out why some API test is failing? Whenever something like that happens to me, my immediate thought is: what could have helped me find this problem faster?&lt;/p&gt;

&lt;p&gt;Unit tests are much better at this, when they fail, you just run them again locally, set a breakpoint if you have to, and find out what failed. But with tests that run against your API (yes, you need those too!) it can be much harder to figure out exactly what code got executed. There are all sorts of boundaries that make this hard, especially when those tests run as part of your pipeline (yes, you need to do that!). You can’t just set a breakpoint in your pipeline and even if you have logging in your backend, how do you know which log entries resulted from which test scenario?&lt;/p&gt;

&lt;p&gt;I’ve been playing with the idea of linking backend logs to tests for some time. Years back, I had a plan to add the name of the test to a custom HTTP header when calling the API from tests. In the backend I could add that value to the MDC context that we attach to log entries. The result would be that for each log entry, you could see which test triggered it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Déjà vu
&lt;/h2&gt;

&lt;p&gt;Somehow I never got around to implementing that idea. Last year though, I helped out a team working on an API-only service that ran into the very same problem. Their system is comprised of a set of microservices and some of the API calls go through several of these services to handle the request. Next to unit tests, one of their other automated validations is a regression test suite that runs against the API to validate expected behavior.&lt;/p&gt;

&lt;p&gt;So far, so good, and that test suite was really valuable. It allowed the team to quickly implement new functionality without spending a lot of time checking that existing functionality kept working.&lt;/p&gt;

&lt;p&gt;You know what’s coming now… whenever a test would fail, it was a nightmare to figure out why.&lt;/p&gt;

&lt;p&gt;Luckily, I had just finished hooking up all the services to an &lt;a href="https://www.elastic.co/cloud/" rel="noopener noreferrer"&gt;Elastic Cloud&lt;/a&gt; environment, with &lt;a href="https://www.elastic.co/observability/application-performance-monitoring" rel="noopener noreferrer"&gt;APM-powered tracing&lt;/a&gt; and all the good stuff. The traces gave excellent insight into what happened inside and between the microservices. However, it was still really hard to link a specific test failure to a set of traces.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenTelemetry FTW
&lt;/h2&gt;

&lt;p&gt;I still had that idea from before in my head, and when I saw the team struggling with these failing tests, a new idea popped in: what if I linked the traces to tests?&lt;/p&gt;

&lt;p&gt;The typical method to pass trace IDs across service calls is by using a trace header. OpenTelemetry provides the &lt;code&gt;traceparent&lt;/code&gt; header for that, and the &lt;a href="https://www.elastic.co/guide/en/apm/agent/index.html" rel="noopener noreferrer"&gt;Elastic APM agent&lt;/a&gt; being used in the services &lt;a href="https://www.elastic.co/guide/en/apm/agent/java/current/supported-technologies-details.html" rel="noopener noreferrer"&gt;picks that up out-of-the-box&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My first idea was to return the &lt;code&gt;traceparent&lt;/code&gt; header in the API responses and then log them with each test. However, that’s not really how the OpenTelemetry &lt;code&gt;traceparent&lt;/code&gt; header is supposed to be used. You’re supposed to include the header in the calling request. So that’s what I tried next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show me the code!
&lt;/h2&gt;

&lt;p&gt;First thing I did was locate how the test suite was making the API calls. Turned out this was done with &lt;code&gt;HttpClient&lt;/code&gt;, which offers a nice way to inject additional headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openTelemetry&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HttpClientTraceInstrumentation&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;HttpProcessor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nd"&gt;@Override&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;injectTraceHeaders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="nd"&gt;@Override&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;injectTraceHeaders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="c1"&gt;// TODO&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt; &lt;span class="nf"&gt;registerTraceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="nc"&gt;DefaultHttpClient&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;HttpClientTraceInstrumentation&lt;/span&gt; &lt;span class="n"&gt;traceInstrumentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClientTraceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addRequestInterceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;traceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addResponseInterceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;traceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, so that gives us a way to inject the &lt;code&gt;traceparent&lt;/code&gt; header. First I figured I could generate the trace ID myself. However, it turns out &lt;a href="https://www.w3.org/TR/trace-context/#traceparent-header-field-values" rel="noopener noreferrer"&gt;the &lt;code&gt;traceparent&lt;/code&gt; value&lt;/a&gt; is not just a trivial UUID, it has a very specific structure and I wouldn’t advise trying to generate it yourself.&lt;/p&gt;

&lt;p&gt;Luckily, OpenTelemetry has excellent SDKs that make it easy to generate and inject these headers. So we need a dependency on the OpenTelemetry API and SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependencyManagement&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.opentelemetry&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;opentelemetry-bom&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.18.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;pom&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;import&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencyManagement&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.opentelemetry&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;opentelemetry-api&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.opentelemetry&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;opentelemetry-sdk&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we had to initiate a trace for each scenario, and a sub-span for each API call being made.&lt;/p&gt;

&lt;p&gt;After looking at the codebase, we saw we could add that to an already present base class used by all tests to report scenario results to &lt;a href="https://www.gurock.com/testrail/" rel="noopener noreferrer"&gt;TestRail&lt;/a&gt;. Personally I’m not a big fan of using base classes for tests as it can be hard to maintain a sensible hierarchy and the use of inheritance creates tight coupling between tests, which we don’t want. In JUnit for example, I would use &lt;code&gt;@ExtendWith&lt;/code&gt; to keep such responsibilities separated and easier to re-use and to avoid the use of static instances as seen here. But we didn’t want to overhaul the complete test suite just to introduce this, better not to mix a big refactoring like that. The main goal at this point was to validate that traces could work.&lt;/p&gt;

&lt;p&gt;As you see, we also passed the &lt;code&gt;traceId&lt;/code&gt; to TestRail, more on that later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hooks&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseSteps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;TestRailReporter&lt;/span&gt; &lt;span class="n"&gt;testRailReporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="nc"&gt;TestRailReporterFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ScenarioTraceInstrumentation&lt;/span&gt; &lt;span class="n"&gt;instrumentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ScenarioTraceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

 &lt;span class="nd"&gt;@Before&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;instrumentation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startTrace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="nd"&gt;@After&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;teardown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instrumentation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;endTrace&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;testRailReporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reportResultToTestRail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For every scenario we start a new span, including attributes to link it to the scenario. We also do some rudimentary logging for every scenario, making it easy to spot in the logs which scenario ran at which point and what the trace ID was for that scenario run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ScenarioTraceInstrumentation&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;startTrace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;Span&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spanBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSpanKind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpanKind&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CLIENT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test.scenario_id"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

 &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"|---------\n"&lt;/span&gt;
 &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"| SCENARIO: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" - "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"\n"&lt;/span&gt;
 &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"| trace.id: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpanContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTraceId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;endTrace&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;traceId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To keep track of span state, I used a &lt;code&gt;ThreadLocal&lt;/code&gt;, exposing a simple API to start and end the span that we could use from the &lt;code&gt;ScenarioTraceInstrumentation&lt;/code&gt; and &lt;code&gt;HttpClientTraceInstrumentation&lt;/code&gt;. As you see I did get lazy a bit here, using a &lt;code&gt;Pair&lt;/code&gt; to store both the &lt;code&gt;Span&lt;/code&gt; and &lt;code&gt;Scope&lt;/code&gt;, using a record for that would make the code easier to read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pair&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Span&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Scope&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

 &lt;span class="nc"&gt;Span&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpanBuilder&lt;/span&gt; &lt;span class="n"&gt;spanBuilder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;Span&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spanBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startSpan&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="nc"&gt;Scope&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;makeCurrent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Pair&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;traceId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getLeft&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getSpanContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTraceId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;Pair&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Span&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Scope&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRight&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLeft&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OpenTelemetry SDK also needs some initialization. Keeping it simple for this first experiment, and since some of the other code was static already, we just created two package scoped statics to be used from the &lt;code&gt;ScenarioTraceInstrumentation&lt;/code&gt; and &lt;code&gt;HttpClientTraceInstrumentation&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetry&lt;/span&gt; &lt;span class="n"&gt;openTelemetry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;initOpenTelemetry&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Tracer&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openTelemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTracer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"regressionTest"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetry&lt;/span&gt; &lt;span class="nf"&gt;initOpenTelemetry&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;SdkTracerProvider&lt;/span&gt; &lt;span class="n"&gt;sdkTracerProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SdkTracerProvider&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="nc"&gt;OpenTelemetrySdk&lt;/span&gt; &lt;span class="n"&gt;sdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetrySdk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTracerProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sdkTracerProvider&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPropagators&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContextPropagators&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="nc"&gt;W3CTraceContextPropagator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
 &lt;span class="o"&gt;))&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="nc"&gt;Runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRuntime&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addShutdownHook&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;sdkTracerProvider:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, we could put all the pieces together in the &lt;code&gt;HttpClientTraceInstrumentation&lt;/code&gt;, see below.&lt;/p&gt;

&lt;p&gt;We create an additional span for the request [1] and then inject the trace header into the request [2].&lt;/p&gt;

&lt;p&gt;The OpenTelemetry SDK code is very generic, allowing you to inject headers into pretty much anything, from HTTP requests to messages with all sorts of client libraries. The extremely generic code does make it a bit hard to read, but essentially you need to pass it a method that accepts two string arguments, the header name and header value. The &lt;code&gt;HttpRequest&lt;/code&gt; from &lt;code&gt;HttpClient&lt;/code&gt; has exactly such a method: &lt;code&gt;setHeader&lt;/code&gt; [3].&lt;/p&gt;

&lt;p&gt;Last but not least, we also log every request being made [4], giving a nice overview in the test logs of what API calls are being made for each scenario.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openTelemetry&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HttpClientTraceInstrumentation&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;HttpProcessor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;W3C_TRACEPARENT_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Traceparent"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;TextMapPropagator&lt;/span&gt; &lt;span class="n"&gt;textMapPropagator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="n"&gt;openTelemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPropagators&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTextMapPropagator&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;TextMapSetter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="nl"&gt;HttpRequest:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;setHeader&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// [3] http client setHeader method&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

 &lt;span class="nd"&gt;@Override&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;Span&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spanBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSpanKind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpanKind&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CLIENT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scope&lt;/span&gt; &lt;span class="n"&gt;ignored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;makeCurrent&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="c1"&gt;// [1] add span details&lt;/span&gt;
 &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;HTTP_METHOD&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRequestLine&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getMethod&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
 &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;HTTP_URL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRequestLine&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getUri&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
 &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"component"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"http"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

 &lt;span class="n"&gt;injectTraceHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="n"&gt;logRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="nd"&gt;@Override&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;injectTraceHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="c1"&gt;// [2] let opentelemetry sdk propagate any required headers&lt;/span&gt;
 &lt;span class="n"&gt;textMapPropagator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setter&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;logRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// [4]&lt;/span&gt;
 &lt;span class="s"&gt;"|- "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRequestLine&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
 &lt;span class="s"&gt;"| ^- "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLastHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;W3C_TRACEPARENT_HEADER&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Everything coming together
&lt;/h2&gt;

&lt;p&gt;Now when tests are run, their logs clearly show each of the scenarios, their trace ID and the API calls being invoked. And for every API call, the complete &lt;code&gt;traceparent&lt;/code&gt; header is shown. With these details being propagated to our backend, we can now look up all the calls for a scenario in Elastic, or even search there for the span ID to locate the trace for a specific API call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|---------
| SCENARIO: T2748705 - Validate if method PUT was added to controller and it's possible to use it
| trace.id: d28a64332015de5d324cb3e0f0380eba
|- PUT http://.../api/...
| ^- traceparent: ...
...
...
|---------
| SCENARIO: ...
| trace.id: ...
|- GET http://.../api/...
| ^- traceparent: ...
...
...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Integrate existing tools
&lt;/h2&gt;

&lt;p&gt;At this client, the team used &lt;a href="https://www.gurock.com/testrail/" rel="noopener noreferrer"&gt;TestRail&lt;/a&gt; to centrally view reports of all test runs. To find the cause for a failing test, wouldn’t it be nice if we could skip sifting through logs? As you saw before, we had the trace ID available when we posted results to TestRail, so we added a link there to take you directly to the corresponding trace in Elastic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc07hoomzdn9w73bjvygt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc07hoomzdn9w73bjvygt.webp" alt="Link to Elastic trace integrated into TestRail" width="720" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results!
&lt;/h2&gt;

&lt;p&gt;When you follow that link to Elastic, it shows you all the API calls (transactions) that were part of that test scenario (trace).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcm3dqij5pyi9ghlqni72.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcm3dqij5pyi9ghlqni72.webp" alt="Trace in Elastic" width="720" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there you can navigate to each API call and look at individual traces. Elastic shows a lot of useful info, like a latency distribution chart that makes it super easy to quickly select all slow calls or all failing calls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyx1mre8z78gu9oxwb8vj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyx1mre8z78gu9oxwb8vj.webp" alt="Trace details in Elastic" width="720" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also a dedicated errors section, which allows you to quickly locate details for any errors that occurred as part of the scenario traces.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg71ammjzh5akiyot3lvq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg71ammjzh5akiyot3lvq.webp" alt="Trace errors in Elastic" width="720" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tip: If you want to take a closer look at what’s in these kind of traces, there is a nice &lt;a href="https://www.elastic.co/demos" rel="noopener noreferrer"&gt;free Elastic demo environment&lt;/a&gt; where you can play around with traces in the APM section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;With just a little bit of code, we managed to reduce the amount of time needed to pinpoint test failures dramatically. Which shows again that optimizing for quick work loops and short feedback cycles are crucial for improving software delivery performance.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>productivity</category>
      <category>testing</category>
    </item>
    <item>
      <title>Move Faster with New Feature Flag Capabilities in AWS</title>
      <dc:creator>Nico Krijnen</dc:creator>
      <pubDate>Mon, 13 Dec 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/realworldarchitect/move-faster-with-new-feature-flag-capabilities-in-aws-37cj</link>
      <guid>https://dev.to/realworldarchitect/move-faster-with-new-feature-flag-capabilities-in-aws-37cj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.luminis.eu/blog/move-faster-with-new-feature-flag-capabilities-in-aws/" rel="noopener noreferrer"&gt;Luminis&lt;/a&gt; on 13 Dec 2021.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I always get excited about things that allow you to bring new ideas to production faster. And it seems we are in luck! During the yearly AWS re:Invent conference that took place in Las Vegas, AWS launched several services that help you do just that!&lt;/p&gt;

&lt;p&gt;Business around us is changing more and more rapidly. Year by year, the State of Devops research confirms that high performing companies excel in adapting to those changes by optimizing their ability to experiment, deliver quickly and gather feedback directly. You need to try many ideas and quickly find out which are successful and which are not. That ability to make many small changes reduces risk and increases the ability to innovate.&lt;/p&gt;

&lt;p&gt;The new AWS services around feature flags enable organizations to achieve these goals. I will explain these briefly and show you how you can leverage them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving fast
&lt;/h2&gt;

&lt;p&gt;So what ingredients do you need to go fast? First, you have to optimize your organization and teams &lt;a href="https://www.linkedin.com/posts/nicokrijnen_adopting-an-experimental-mindset-mark-rickmeier-activity-6876081797009022976-S-XA" rel="noopener noreferrer"&gt;so they can come up with diverse ideas&lt;/a&gt;. Make sure you enable them to try those out and get feedback quickly without having to go through committees to get approval.&lt;/p&gt;

&lt;p&gt;Second, on the engineering side, you need to learn to &lt;a href="https://www.linkedin.com/posts/nicokrijnen_how-the-right-architecture-can-simplify-migrating-activity-6871732324489277440-pe35" rel="noopener noreferrer"&gt;do everything in small steps&lt;/a&gt;. Integrate all your changes with the rest of your team often, at least daily. This way of working is called trunk-based development. The key here is never letting go of high quality and keeping your software in a constant state of being able to deploy it to production. To achieve that, you need a (continuous) delivery pipeline that automates all the steps needed to get from commit to deploy. As &lt;a href="https://youtu.be/x9l6yw1PFbs?t=851" rel="noopener noreferrer"&gt;excellently explained by Dave Farley&lt;/a&gt;, the key responsibility of that pipeline is not to ensure that the software is 100% correct – that is not realistic. Instead, the pipeline needs to do all that it can to figure out if something might be broken or misbehaving. That means you’ll need at least a solid automated test suite that provides you with confidence that on the functional side everything works. Next to that, you can add validations to your pipeline for things like performance, code quality, and any other aspects that are important for your application.&lt;/p&gt;

&lt;p&gt;If you do everything in small steps, does that mean you cannot have big innovations? No, of course it doesn’t. Big innovations never happen in one go. They are the combination of smaller increments, even the failed ones that you learned from. Like Edison’s light bulb, which was the result of thousands of failed experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback, as early as possible
&lt;/h2&gt;

&lt;p&gt;A common trick is to separate the deployment of changes from the release of a feature. By using feature flags, you can include the new code you are writing without the feature showing up or being active. With all the code being deployed continuously, the act of “releasing” a feature simply becomes toggling the feature flag.&lt;/p&gt;

&lt;p&gt;This way of working helps greatly in catching mistakes or wrong assumptions in your ideas early, while it is still easy to try another approach. It is much better to discover that your idea does not work after a few hours than finding out weeks later, when you’ve already invested a lot of time. You also make it much easier to experiment with new features much earlier in their development, maybe just exposing them to a few interested beta users and receive feedback before you make the feature generally available.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk2n0ukl84g7irkxvcvy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk2n0ukl84g7irkxvcvy.webp" width="720" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter feature flags
&lt;/h2&gt;

&lt;p&gt;I always like keeping things simple. The easiest way to implement feature flags is to just add some booleans and ‘if statements’ in your code and allow these to be toggled between the different environments you have. For example, showing the new feature you are working on in your staging environment, but hiding and deactivating it in production. To be honest, in many cases, a simple mechanism like this is more than sufficient.&lt;/p&gt;

&lt;p&gt;Do realize there is a tradeoff when using feature flags. Having these branches in your code means you add complexity and you make your application less predictable. In many cases the benefits greatly outweigh the downside, as long as you keep it under control. Don’t let hundreds of feature flags linger around. Be responsible, be a good craftsman and keep your codebase clean. Once a feature is released, clean up all traces of the flag. That way you minimize complexity and keep your codebase lean and mean and easy to change. After all, you want to keep doing more experiments and keep delivering value in a sustainable way.&lt;/p&gt;

&lt;p&gt;Depending on how long your build pipeline takes, having to change booleans in code may mean that it takes a while to toggle a feature in production. This may be ok in some cases, but when it turns out that a feature doesn’t play nice on production yet, you want to toggle it off again quickly. What if you could toggle it in real-time and let someone else handle part of the complexity around feature flags?&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch 1: AWS AppConfig – Feature Flags
&lt;/h2&gt;

&lt;p&gt;AWS AppConfig has been around since 2019 and allows you to roll out configuration changes to your applications in real-time, without having to deploy code or take your application out of service. It can validate and then roll out configuration changes gradually while monitoring your application and rolling back changes in case of errors, minimizing impact to users.&lt;/p&gt;

&lt;p&gt;Recently AWS launched AWS AppConfig Feature Flags, which builds on AppConfig and makes it easier to roll out new configurations of your feature toggles. You can now setup and toggle feature flags directly from the AWS Console. Versions of your feature flags configuration are tracked and as with config changes, you can manage and monitor the gradual deployment to your environments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjriw4p6dypodfnot2fj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjriw4p6dypodfnot2fj.webp" alt="Feature flags including attributes in AWS AppConfig" width="720" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As feature flags are just booleans, there is no validation around them. You can also add custom attributes to flags. An example of what I would use these for is creating flags that are only enabled for specific beta tenants. For these attributes you can set up some simple validation rules in the UI. That may not be as powerful as writing a validation lambda as you can do for normal AppConfig validation, but it sure is a lot simpler and probably sufficient. And don’t forget, less handcrafted code means you reduce risk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7iw2xvjgq119m126o4u2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7iw2xvjgq119m126o4u2.webp" alt="Feature flag attributes in AWS AppConfig" width="720" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Apart from feature flags, you can also use this feature to toggle operational aspects of your application quickly. For example, in case of an incident you can temporarily switch off non-critical functionality or behavior to reduce load and keep the essentials of your service working, allowing your team to focus on resolving the incident.&lt;/p&gt;

&lt;p&gt;That all sounds fantastic, but it does mean that releases of features are no longer tracked in your central git history, instead they are changed through the AWS Console. Luckily, the Console does track versions of your configurations and shows you the history of feature flags and configuration deployments that have been done to each of your environments. So, it may not be in one place anymore, but you can still audit when features went live or were rolled back. Just be aware that you are making this tradeoff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs047g650iw0cdqzpttk4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs047g650iw0cdqzpttk4.webp" alt="AWS AppConfig deployment history" width="720" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch 2: AWS CloudWatch – Evidently
&lt;/h2&gt;

&lt;p&gt;Where AppConfig Feature Flags is geared towards simplifying the technicality of managing feature flags and their deployment, Evidently is all about connecting the data you collect for experiments and A/B tests around feature roll outs that you do with smaller sets of users before making the feature generally available.&lt;/p&gt;

&lt;p&gt;Evidently allows you to set up multiple variants of a feature and test these variants on different subsets of users at the same time. Once you have set up and are running such an experiment, Evidently also helps in statistically analyzing which variant is performing better, based on data collected during the experiment, giving you confidence in making the right choices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8cu20dquuogkwgnwk4vp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8cu20dquuogkwgnwk4vp.webp" alt="AWS CloudWatch Evidently getting started with experiments" width="720" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In today’s fast paced world, you have to continuously reinvent your business to remain relevant. To do so you have to innovate and Evidently really helps you with tools to apply that mindset of running experiments and basing your decisions on the data that you gather. In today’s fast paced world, you have to continuously reinvent your business to remain relevant. To do so you have to innovate and Evidently really helps you with tools to apply that mindset of running experiments and basing your decisions on the data that you gather.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7sh4ccj6mhxm36ppv3rp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7sh4ccj6mhxm36ppv3rp.webp" alt="AWS CloudWatch Evidently audience distribution" width="720" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch 3: Amazon CloudWatch RUM (Real User Monitoring)
&lt;/h2&gt;

&lt;p&gt;Now obviously, to make a service like Evidently work well, you need to collect the right data. Data that helps you prove the effectiveness and desired behavior of the features you build. To help with this, AWS launched RUM, another service under the CloudWatch umbrella.&lt;/p&gt;

&lt;p&gt;CloudWatch is all about monitoring, but so far that was mostly oriented towards raw infrastructure and backend metrics of apps running inside AWS. RUM enhances that with capabilities to monitor web application performance, aimed at user experience. You inject some JavaScript into your web application frontend which then allows you to anonymously collect page load and layout performance metrics, JavaScript errors and client-side experienced HTTP errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21aawho10k8pci1c69wl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21aawho10k8pci1c69wl.webp" alt="AWS CloudWatch RUM getting started" width="720" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where traditional CloudWatch metrics are somewhat raw, RUM feels more like Google Analytics, giving you out-of-the-box dashboards that provide visibility into your application’s reliability, performance, and end user satisfaction. The service performs analysis on the collected data and breaks it down so you can gain insights about which areas of your application’s user experience can be improved.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbge8hyj48ko7hw7x7woc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbge8hyj48ko7hw7x7woc.webp" alt="AWS RUM average load times" width="720" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the observability side, CloudWatch RUM integrates with AWS X-Ray, so you can view traces and segments for end user requests. You can even see your clients in CloudWatch ServiceLens service maps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojci13hnmjvnutbbmhpy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojci13hnmjvnutbbmhpy.webp" alt="AWS RUM tracing with AWS X-Ray" width="720" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;As usual with AWS, pricing for these services are based on usage, which means you can try these out on a small scale and then evaluate if the benefits they bring you are worth the cost. Once you use these services in production, always put up some cost monitoring so you remain aware of what you are spending. Below you will find an overview of the pricing for these services. For details, look at their pricing page.&lt;/p&gt;

&lt;p&gt;AWS AppConfig Feature Flags are charged by the number of times you request and receive config changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$0.0000002 per configuration request via API Calls&lt;/li&gt;
&lt;li&gt;$0.0008 per configuration received&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS CloudWatch Evidently has a first time free trial that includes 3 million Evidently events and 10 million Evidently analysis units per account. After that you pay:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$5 per 1 million events (user actions &amp;amp; assignment events)&lt;/li&gt;
&lt;li&gt;$7.50 per 1 million analysis units&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS CloudWatch RUM allows you to control costs by configuring it to only analyze a limited percentage of user sessions. There is a first time free trial that includes 1 million RUM events per account. After that you pay:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$1 for every 100,000 events collected (page view, JavaScript error, HTTP error)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Alternatives
&lt;/h2&gt;

&lt;p&gt;Compared to products that specialize in this field like LaunchDarkly or Optimizely, these AWS services may feel somewhat bare bones. This is no coincidence, as Werner Vogels said: AWS services are designed to be building blocks, not frameworks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn2p4q9z7tfs08aqd0ac.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn2p4q9z7tfs08aqd0ac.webp" alt="LaunchDarkly showing current state of feature flag in code editor" width="720" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS may not be as feature rich as some of these others but having it all integrated into the rest of your service stack can be a big benefit. The low barrier to entry means you can start using it today and get value out of it quickly, or switch to something else later if you discover that your needs have grown.&lt;/p&gt;

&lt;p&gt;And don’t forget, if you don’t need all the experimentation statistics or real-time flag controls, just keep it simple and add a few booleans in your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It is great to see that AWS is serious about giving their users the keys they need for innovation. What I personally find most exciting is that with this combination of services, AWS makes it easier for all of us to adopt that mindset of doing experiment-based and data-driven roll outs.&lt;/p&gt;

&lt;p&gt;If you need help to introduce that mindset of experimentation, don’t hesitate to reach out!&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;

&lt;p&gt;If you want to learn more about these AWS services, check out the official blogs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/mt/introducing-aws-appconfig-feature-flags-in-preview/" rel="noopener noreferrer"&gt;Introducing AWS AppConfig Feature Flags In Preview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/cloudwatch-evidently/" rel="noopener noreferrer"&gt;New – Amazon CloudWatch Evidently – Experiments and Feature Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/cloudwatch-rum/" rel="noopener noreferrer"&gt;New – Real-User Monitoring for Amazon CloudWatch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>news</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
