惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
Apple Machine Learning Research
Apple Machine Learning Research
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
博客园 - 聂微东
罗磊的独立博客
W
WeLiveSecurity
博客园_首页
Scott Helme
Scott Helme
V
Visual Studio Blog
T
The Exploit Database - CXSecurity.com
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
Latest news
Latest news
L
Lohrmann on Cybersecurity
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
A
About on SuperTechFans
F
Full Disclosure
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园 - 司徒正美
博客园 - Franky
C
CXSECURITY Database RSS Feed - CXSecurity.com
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
阮一峰的网络日志
阮一峰的网络日志
S
Schneier on Security
雷峰网
雷峰网
博客园 - 【当耐特】
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
Engineering at Meta
Engineering at Meta
aimingoo的专栏
aimingoo的专栏
MongoDB | Blog
MongoDB | Blog
J
Java Code Geeks
T
Tor Project blog
V
V2EX
爱范儿
爱范儿
C
Check Point Blog
T
Threatpost
Project Zero
Project Zero
量子位
V
Vulnerabilities – Threatpost
Know Your Adversary
Know Your Adversary
I
Intezer
G
GRAHAM CLULEY
P
Privacy & Cybersecurity Law Blog
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

Datadog | The Monitor blog

Introducing our open source AI-native SAST Instrument and monitor Boomi integration flows with OpenTelemetry and Datadog Not all index scans are equal: How we cut query latency by over 99% Platform engineering metrics: What to measure and what to ignore Integrate Recorded Future threat intelligence with Datadog Cloud SIEM CI/CD security: threat modeling using a MITRE-style threat matrix CI/CD security: How to secure your GitHub ecosystem Ingress NGINX is EOL: A practical guide for migrating to Kubernetes Gateway API Operating agentic AI with Amazon Bedrock AgentCore and Datadog LLM Observability: Lessons from NTT DATA Introducing the Datadog Code Security MCP Capture and analyze custom heatmaps in Session Replay Understand session replays faster with AI summaries and smart chapters Monitor ClickHouse query performance with Datadog Database Monitoring How we designed empathetic alert sounds for on-call engineers Search and act across Datadog to resolve issues faster with Bits Assistant Measure the business impact of every product change with Datadog Experiments Analyzing round trip query latency Configuring JavaScript caches for better performance Introducing Bits AI Dev Agent for Code Security Datadog achieves ISO 42001 certification for responsible AI Monitor Nutanix clusters, hosts, and VMs with Datadog Monitor Juniper Mist in Datadog A new Host Map for modern infrastructure Annotate traces to improve LLM quality with Datadog LLM Observability What’s new in Cloud SIEM: AI-powered investigations, enhanced threat intelligence, and scalable security operations Explore Kubernetes with native OpenTelemetry data Monitor Oracle Fusion Cloud Applications with Datadog Announcing the Datadog Terraform provider v4.0.0 Scaling Kubernetes workloads on custom metrics How to design cloud environments for AI-powered threat analysis Monitor Aruba Central in Datadog How we centralize and remediate risks with Datadog Case Management Accelerate incident response with Datadog and ServiceNow Monitor your application and network load balancer logs Understanding Karpenter architecture for Kubernetes autoscaling Tools for collecting metrics and logs from Karpenter Monitor Karpenter with Datadog What your product data is actually saying Key metrics for monitoring Karpenter Securing Datadog’s platform in the AI age: The role of observability data Four ways engineering teams use the Datadog MCP Server to power AI agents Approaching your observability migration with the right mindset Meet the new Bits AI SRE: Deeper reasoning, twice as fast Key learnings from the 2026 State of DevSecOps study Use plain English to query your multi-cloud infrastructure in Resource Catalog Simplifying troubleshooting across the user journey with Datadog Synthetic Monitoring Protect your OCI resources with Datadog Cloud Security This Month in Datadog - February 2026 Amazon EC2 security: How misconfigured and public AMIs expand your cloud attack surface Enable end-to-end visibility into your Java apps with a single command Measure and improve mobile app startup performance with Datadog RUM Evaluating our AI Guard application to improve quality and control cost Identify untested code across every level of your codebase Make use of guardrail metrics and stop babysitting your releases Monitor Versa Networks SD-WAN performance in Datadog Improve performance and reliability with APM Recommendations Remediate transitive vulnerabilities faster with Datadog Software Composition Analysis Generate audit-ready vulnerability and compliance reports with Datadog Sheets Monitor Fortinet FortiManager performance in Datadog Improve test coverage across codebases with Datadog Code Coverage Move fast, don’t break things: Consistent testing standards at scale Enrich logs with ServiceNow CMDB context before routing to any SIEM or logging tool Monitor Lustre with Datadog Make faster, better product decisions with Datadog Product Analytics Surface and remediate runtime posture issues with Workload Protection Findings Protect agentic AI applications with Datadog AI Guard How to optimize JavaScript code with CSS Trace Google Pub/Sub workloads in Cloud Run with Datadog Detect human names in logs with ML in Sensitive Data Scanner How we cut our NLQ agent debugging time from hours to minutes with LLM Observability Debug PostgreSQL query latency faster with EXPLAIN ANALYZE in Datadog Database Monitoring Datadog acquires Propolis Unify and correlate frontend and backend data with retention filters Scale compliance across global frameworks with Datadog Cloud Security Monitor Arista VeloCloud SD-WAN performance with Datadog Building reliable dashboard agents with Datadog LLM Observability Simplify log collection and aggregation for MSSPs with Datadog Observability Pipelines Mitigation for Node.js denial-of-service vulnerability affecting Datadog APM Automate flaky test fixes with the Bits AI Dev Agent and Test Optimization How we built an AI SRE agent that investigates like a team of engineers Datadog integrations 2025 recap: Observability for AI, security, and hybrid cloud Design effective executive dashboards with Datadog Implement dbt data quality checks with dbt-expectations Bring faster visibility into AWS Lambda functions with remote instrumentation Troubleshoot faster with the GitLab Source Code integration in Datadog How Cambia Health Solutions saved $30,000 monthly with Cloud Cost Management and the Datadog Resource Catalog Normalize any logs for Cloud SIEM with Datadog's OCSF processor Optimizing Datadog at scale: Cost-efficient observability at Zendesk Detect, diagnose, and resolve network issues easily with CNM Network Health Connect engineering errors to user impact in early-stage products Cilium configuration for Kubernetes operations at scale Designing feedback loops for progressive delivery Ship features faster and safer with Datadog Feature Flags Choosing the right OpenTelemetry Collector distribution Route your monitor alerts with Datadog monitor notification rules Automate Cloud SIEM investigations with Bits AI Security Analyst Cloud threat detection: How to identify risky activity across control and data planes Collecting Kafka performance metrics Monitoring Kafka with Datadog Monitoring Kafka performance metrics
Steganography at scale: Embedding share URLs in Datadog widget screenshots
Bartol Rebernjak, Galen Pickard · 2026-04-21 · via Datadog | The Monitor blog

Sharing a visualization from a Datadog dashboard or notebook is simple. You can copy and paste a widget directly into another dashboard, message, or document. Behind the scenes, the widget is stored with a unique sharing URL, which the Datadog app or chat integrations can use to render a live preview or link back to the underlying data.

These live previews are especially powerful when using Datadog Incident Management’s docked view. When an incident is docked, pasting a widget into a Slack incident channel renders it as a fully interactive widget in Datadog, letting users correlate key indicators from an incident with other metrics during investigation and remediation.

A shared widget renders inside a docked incident view, letting responders correlate signals during an investigation.
Screenshot of a Datadog dashboard with a docked incident view beside a Slack incident channel. A shared widget pasted into Slack is rendered in Datadog as an interactive graph alongside other incident context, allowing responders to correlate web store errors with related dashboard signals.
A shared widget renders inside a docked incident view, letting responders correlate signals during an investigation.

Although this feature captures full context for visualizations, many users still prefer to take screenshots.

Screenshots are quick, intuitive, and guarantee a consistent visual snapshot, regardless of context or permissions. However, taking screenshots loses a lot of valuable information, such as the time range, underlying query, visualization type, and the dashboard state at the moment the image was taken.

This led us to a question: What if screenshots could carry the same context as share links, without changing how they look?

In this post, we’ll show you how we built a system to invisibly embed widget metadata inside a screenshot using a pixel-level encoding scheme. We’ll cover:

  • How we compress, encode, and render metadata without affecting the user experience

  • Why we needed to build resilient watermarking that survives color profiles, display densities, and copy-paste workflows

  • The backend systems we use to cache and retrieve this metadata

  • Performance optimizations that let us render over a billion watermarks per day without impacting app responsiveness

Along the way, we’ll share what worked, what didn’t, and how we balanced scale, resilience, and invisibility to build a feature that’s always on, but only noticeable when you need it.

How it works today

When you copy a widget in Datadog, we generate a backend record containing the widget definition and store a reference to it in your clipboard as a share URL. Pasting this URL into another dashboard or notebook restores the original widget. If you paste the URL into Slack or Teams, the Datadog chat integration displays a preview of the graph and its title:

When a Datadog share URL is pasted into Slack, it renders a live preview of the widget—complete with the metric, title, and a snapshot of the graph.
Screenshot of a Slack message showing a Datadog share URL, the metric name avg:system.cpu.user{*}, and a small embedded graph preview with a 3-day time range.
When a Datadog share URL is pasted into Slack, it renders a live preview of the widget—complete with the metric, title, and a snapshot of the graph.

Clicking the link opens the original widget in Graph Explorer, preserving time range, queries, and other configuration details.

Could we make these features work even if you take a screenshot instead of copying the widget? The question came up in one of our internal engineering channels.

More than a dozen engineers from teams across the company chimed in, sharing thoughts on the problem and ideas for a solution. Throughout the discussion, we explored existing approaches to similar problems and a handful of original ideas. Eventually, we landed on something that sounded promising: watermarking.

We wanted a way to embed metadata in a screenshot without affecting the user experience or adding more UI elements to an already crowded dashboard. Maybe we could subtly alter the colors of some pixels inside a widget—invisible to the naked eye, but recoverable by a watermark-reading algorithm.

How much data can we even encode in a watermark?

Widget definitions can be deceptively large. They include all the data requests and queries, display settings, legend configuration, time frame overrides (if present), and the title. If the widget comes from a dashboard, we also want to include the current time frame, template variable values, widget dimensions, and a deep link.

On average, this adds up to around 2 kB per widget. That is a lot of data to encode in a nearly invisible image, especially when it needs to work reliably even for the smallest widgets.

Encoding just enough data

Instead of encoding entire widget definitions, we write them in a Redis cache. Each record contains a full widget definition, stored behind a randomly generated key. That key—a fraction of the record’s size—can be encoded into a watermark.

Widget definitions contain a snapshot of the widget’s current state. So if the widget’s data is refetched, the time frame changes slightly, or its definition is updated, we generate a new snapshot, cache it, and embed a new watermark.

Each widget rendered in the browser generates a unique snapshot key, which is cached and later retrieved via a backend service.
Diagram showing widgets in a browser sending data to a service, which stores and retrieves records from Redis.
Each widget rendered in the browser generates a unique snapshot key, which is cached and later retrieved via a backend service.

To ensure that watermarks are displayed as soon as widgets are rendered, we generate the snapshot ID optimistically on the frontend before the record is actually cached. This avoids waiting for the service response, but introduces a small risk of ID collisions.

How small? Across all production data centers, we render just over 1 billion widgets per day, with peaks around 125 million widgets per hour (about 35,000 per second). Most screenshots are taken and pasted within a few seconds, and almost never more than a few minutes later. So we keep all snapshot records cached for one hour, allowing us to detect and recover watermarks from recently rendered widgets.

Smaller IDs are easier to hide invisibly in an image, but at that scale, small IDs introduce a non-trivial collision risk. The risk of two of your widgets receiving the same ID needs to be small, but the risk of one of your widgets receiving the same ID as a widget from another Datadog customer needs to be zero. To guarantee this, we construct our Redis cache key by prepending an organization ID to the widget ID.

No single customer accounts for more than 1% of our widget renders, so we can use 1 million IDs per hour as a reasonable upper bound. 1 million IDs means 5×10¹¹ ID pairs, and an 8-byte ID gives us 2⁶⁴ ≈ 1.8×10¹⁹ possible values. Dividing these two numbers gives us an approximate probability of collision: roughly 1 in 37 million.

But how do we invisibly embed 8 bytes of data into a dashboard widget?

Every dashboard widget has a thin, 1 px border of a uniform color. While the widget contents vary a lot depending on the chosen visualization and configuration, the border is always there. That made it an ideal place to embed the watermark without having to implement any visualization-specific logic.

Our first idea was simple: Line the top or the bottom border (they are usually longer than side borders) with pixels in two different colors, each representing a single bit.

A Datadog widget with a thin, uniform border that provides a consistent surface for embedding invisible watermark data.
Line graph titled “Kafka offset lag (by topic,partition)” with colorful time series plotted over a fifteen-minute window. Shows the type of widget where border-level watermarking is applied.
A Datadog widget with a thin, uniform border that provides a consistent surface for embedding invisible watermark data.

Unfortunately, for 8 bytes of data, this would require at least 64 pixels. While our smallest widget is still wider than that, the larger the watermark, the more likely it is to be visible to the eye, even if we use colors similar to the base border color instead of bright red and blue. We wanted smaller watermarks, ideally no longer than 10 pixels. Could we store an entire byte in a pixel?

In the RGB model, each pixel has three color channels. This gave us an opportunity: We split a byte of data into three values and use them to offset the base RGB values of the border, encoding our data directly into the image.

Given a background RGB color, we calculate the base color by subtracting 3 from each channel. Then we increase the values by up to 7, to store up to 9 bits per pixel. For example, increasing R by 5, G by 7, and B by 1, encodes 101-111-001 in bits.

To mark the watermark region, we use two sentinel pixels at the start and the end, encoded as +7/7/7 per RGB channel (111-111-111). So if the base color is #e0e0e0, the sentinel pixels become #e7e7e7.

The eight pixels between those sentinels each store one byte of data, using three bits from the R, three from the G, and the top two bits of the B channel.

This is what that layout looks like:

Watermark layout showing sentinel pixels at the boundaries and eight data pixels encoding one byte each using RGB channel offsets.
Diagram of a horizontal sequence of pixels labeled S, byte 1 through byte 8, and S, illustrating a watermark where sentinel pixels mark the start and end, and the eight inner pixels each encode one byte of data.
Watermark layout showing sentinel pixels at the boundaries and eight data pixels encoding one byte each using RGB channel offsets.

Below is a screenshot of a watermark generated using this method, placed along the top border of a dashboard widget, zoomed in at 8,000%.

A zoomed-in view of watermark-encoded pixels embedded in a widget border, showing subtle color offsets used to store metadata.
Screenshot of a widget border with 10 individual pixels showing slight color differences, illustrating how watermark data is encoded at the pixel level.
A zoomed-in view of watermark-encoded pixels embedded in a widget border, showing subtle color offsets used to store metadata.

The color profile problem

At this point, you might notice a problem with our approach. It would work perfectly, if everyone only used RGB displays.

When we generate a watermark using RGB color values and take a screenshot on an RGB device, we can recover the exact original colors from the image. But many modern displays use other color profiles.

For example, taking a screenshot on a DCI-P3 device (like a Retina MacBook) and converting it back to RGB often shifts some channel values by ±1. The values don’t map properly, and we’re left with ambiguous decimal values that are hard to interpret. For an encoding scheme that relies on exact color values, that’s a deal breaker.

Also, depending on the ID, the watermark was sometimes visible. A casual user wouldn’t notice it, but if you knew where to look, you could spot it. That told us we should decrease the color variance, even if it meant using more pixels.

Solving the color gamut problem with fuzzy encoding

Third time’s a charm.

To make the watermark resilient across different color gamuts, we switched to a fuzzy encoding approach. By mapping each part of the ID to a range of neighboring colors, we could still recover the original value, even if the colors are slightly offset.

With this method, each pixel stores just 2 bits of data. This gives us a short enough watermark (34 pixels wide), while reducing the number of colors needed. Fewer colors means we stay closer to the base color, making the watermark significantly less noticeable.

Given a background RGB color, we could encode up to 2 bits per pixel—one in the red channel and one in the green—by offsetting the base color. During watermark generation, a +3 offset encodes a 1 and -3 encodes a 0. When decoding, offsets in the range of [+1, +5] are interpreted as 1, [-1, -5] as 0, and anything in (-1, 1) as undefined. The blue channel doesn’t carry data but helps distinguish sentinel pixels from data pixels: a valid sentinel has red and green offsets within [–1, 1] and a blue offset within [+1, +5].

As an example, let’s take (226, 229, 237) as our base color. This is the actual border color for dashboard widgets. A base pixel would look like this:

Base pixel color (226, 229, 237) used for watermark encoding—minimizing visual difference while preserving metadata in the red and green channels.
Square image displaying a single RGB pixel with color values 226, 229, 237, which is used as the base color for invisible watermark encoding in dashboard widget borders.
Base pixel color (226, 229, 237) used for watermark encoding—minimizing visual difference while preserving metadata in the red and green channels.

To designate a pixel as a sentinel pixel, which would mark the beginning of a watermark, we would shift that color by +0/+0/+3, resulting in a slightly more blue (226, 229, 240) pixel:

Sentinel pixel color (226, 229, 240), created by offsetting only the blue channel, signals the start of a watermark.
Square image showing a pixel with RGB values 226, 229, 240, used as a sentinel pixel to mark the beginning of an encoded watermark.
Sentinel pixel color (226, 229, 240), created by offsetting only the blue channel, signals the start of a watermark.

Let’s try encoding 10110011, a single byte of data. This would be split into four pixels, two bits per pixel: +3/-3/+0, +3/+3/+0, -3/-3/+0, +3/+3/+0. This results in (229, 226, 237), (229, 231, 237), (223, 226, 237), and (229, 232, 237):

Encoding a single byte (10110011) across four pixels using 2-bit offsets in the red and green channels. Each pixel represents two bits of data.
Horizontal image showing four adjacent pixels with slightly different RGB values: (229, 226, 237), (229, 231, 237), (223, 226, 237), and (229, 232, 237). These represent the 2-bit segments of the byte 10110011.
Encoding a single byte (10110011) across four pixels using 2-bit offsets in the red and green channels. Each pixel represents two bits of data.

Drawing that on top of our background color, wrapped with a sentinel pixel on both sides, gives us this:

Final watermark layout: Four data pixels encoding 10110011, wrapped with sentinel pixels on each side to mark the watermark boundaries.
A row of six pixels embedded in a light gray background. The outer two pixels are sentinel markers, and the four inner pixels represent encoded binary data 10110011 using subtle RGB offsets.
Final watermark layout: Four data pixels encoding 10110011, wrapped with sentinel pixels on each side to mark the watermark boundaries.

You might say it’s pretty visible, but keep in mind this is scaled up about 80x.

We can take this through any color gamut remapping and be sure that converting it back to RGB will keep the values within the same ranges, allowing us to read the originally encoded value.

More tuning for real-world screenshots

At this point, we had a resilient encoding method.

Does that mean our watermark could be picked up reliably from screenshots in any situation? Nope.

During internal testing, we used RUM to find Datadog employees with unusual setups—devices with uncommon screen densities, operating systems, or browsers:

(@usr.is_datadog_employee:true @session.type:user

(-@context.screen.pixel_ratio:1 -@context.screen.pixel_ratio:2) OR

-@os.name:"Mac OS X" OR -@browser.name:Chrome)

We asked them for dashboard screenshots, and their screenshots made clear that our work wasn’t done yet.

Handling different pixel densities

Remember the 1 px dashboard border we mentioned earlier? On one of our screens, it actually takes up 2 px, and it shows up as 2 px thick in the screenshot. Why?

There are a lot of devices browsing the web today. Somewhere out there, someone is still using a 1024x768 CRT monitor from the early 1990s. Someone else is using a 23-million-pixel Apple Vision Pro headset from 2024. The most commonly used unit for laying out web pages is still the CSS pixel. But to make sites look consistent across devices, browsers scale those pixels depending on the screen’s pixel density. This scale is called Device Pixel Ratio (DPR).

On a standard 1920x1080 screen, DPR is 1. On higher-resolution displays (like 14” MacBook Pro), it’s often 2. Some phones even hit 3.

If you specify a CSS width of 100px, it will render as 200 physical pixels on a DPR 2 screen.

Fun fact: DPR is what changes when you zoom in and out with cmd/ctrl + scroll.

To keep the watermark easy to read, we always want it to be exactly one physical pixel tall and 34 pixels wide. On high-DPI screens, this makes it smaller and even less visible. On lower-density displays, it might be more noticeable, but we cannot scale it down without making it unreadable.

In code, this is simple: there’s a global devicePixelRatio value on the window object. We just set the watermark’s height to 1/DPR pixels. In Chrome DevTools, the watermark shows up as 0.5 px tall.

But when we take a screenshot? It is exactly 1 px tall.

A Datadog widget showing a time series graph with an embedded watermark image rendered as a 17×0.5 px element at the bottom of the widget.
Line graph of average system CPU usage with a browser tooltip revealing the embedded watermark image (img.stegadography-watermark.bottom) and its dimensions (17×0.5 px).
A Datadog widget showing a time series graph with an embedded watermark image rendered as a 17×0.5 px element at the bottom of the widget.

Further optimizations

Ultimately, an invisible feature that needs to be enabled all the time—but is useful only occasionally—should not affect overall performance. We don’t want to penalize users who never take screenshots by making their dashboards slower or less responsive.

So we spent some time on performance tuning and implemented a handful of optimizations to minimize overhead.

Choosing how to paint pixels

We tried and tested multiple methods of painting pixels.

An SVG made up of 1x1 rectangles was the easiest thing to generate dynamically in React, and it worked well in our initial prototype. But it has two major problems:

  • It added n+1 DOM elements per watermark, where n is the number of pixels.

  • With two watermarks per widget—and potentially dozens or hundreds of widgets on a dashboard—that’s a lot of DOM overhead.

On top of that, the CPU cost of rendering vector graphics to the screen’s raster added noticeable browser lag.

Surely there had to be a better solution.

Canvas rendering (better, but not great)

The obvious next step was canvas. Since canvas is based on a pixel grid, it’s much better suited for raster imaging.

We tried creating a small canvas for each watermark and drawing pixels with putImageData or fillRect. We also set imageSmoothingEnabled to ensure neighboring pixels didn’t blend together and lose their exact values.

We used feature flags and A/B tested this with RUM. The results were clear: Canvas was faster than SVG in real-world dashboard performance.

Canvas rendering reduced key frontend performance metrics across the board—most notably lowering LCP from 4.34 s to 3.55 s and overall loading time from 11.8 s to 9.5 s.
Performance comparison table titled ‘SVG vs. Canvas performance.’ SVG shows 194k hits, 11.8 s loading time, 4.34 s LCP, 0.03 CLS, and 378 ms INP. Canvas shows 155k hits, 9.5 s loading time, 3.55 s LCP, 0.03 CLS, and 363 ms INP.
Canvas rendering reduced key frontend performance metrics across the board—most notably lowering LCP from 4.34 s to 3.55 s and overall loading time from 11.8 s to 9.5 s.

But it still felt like we were hogging too much CPU and memory just to paint a few pixels on the screen. Even using toDataURL("image/png") to export a canvas as an image would require creating a canvas we didn’t really need.

Generating the pixels ourselves

What if we skipped rendering altogether and just wrote the bytes ourselves?

We considered using a library like jimp to generate images directly, but it’s a pretty big dependency to ship just for a feature that’s meant to be invisible and low-cost.

Our watermarks are simple. How hard could it be to write a few colored pixels into a file?

We looked at the structure of a basic BMP file:

  • BMP Header: Contains a hardcoded signature, file size, and byte offset to the pixel data.

  • DIB Header: Contains header size, width, height, number of color planes (usually 1), bits per pixel, compression, resolution, size of the image data, and the optional color table.

  • Pixel array: Contains pixel data. Based on the bits per pixel, the data is split into individual pixels, with each pixel’s color defined across the red, green, and blue channels.

There is a strong pattern across all the watermark images we generate:

  • Same file size

  • Same byte offset

  • Same header values (size, width, height, number of bits per pixel, no compression, same resolution, and so on)

  • Always 34 pixels

The only thing that changes is the pixel array itself, the RGB values.

After some tweaking, we settled on hardcoded headers. At runtime, we just fill in the rest of the file with bytes for the blue, green, and red channels (yes, in that order—BMP stores them BGR) and generate a Base64 BMP URL to use in the <img/> tag.

Watermarks are now generated with minimal time complexity: a single loop with 34 iterations appending bytes to an array, and a single btoa() call to turn it into a Base64 string.

Sending out caching requests

Remember, we want to create one billion watermarks per day. Every time a widget’s appearance changes for any reason, it’s a new watermark. Every visible watermark needs to reference an entry in the backend cache that contains the corresponding widget definition.

How do we get those definitions into the cache?

If we send out an AJAX request for every watermark, we risk impacting page performance. On a page as request-heavy as a dashboard, this can slow down essential data fetching and processing. We’d also prefer to minimize the number of requests our service receives and the number of cache calls.

We decided to batch the frontend request. Every time a widget is rendered, its watermark is added to the caching queue. Exactly three seconds after the first watermark is added to the queue, all queued entries are submitted to the caching endpoint. This introduces a subtle delay—at most three seconds—but doesn’t harm functionality.

If we are trying to detect widgets in an image and we don’t find any, it’s safe to assume they might just not have been cached yet. Retrying the same cache read five seconds later is usually enough to find any that were delayed.

These caching requests behave differently from most requests in our single-page app. We’re not fetching data to display or storing any user input. The user doesn’t directly care—nor know—whether the request was executed or whether it completed successfully. And neither does the app lifecycle. If the request fails, there’s nothing to do: no error message to show, no state to clean up or adjust.

With that in mind, we moved the request logic out of the React app’s lifecycle to avoid introducing unnecessary context. Instead, we set up a dedicated web worker to manage the queue and send caching requests. Because web workers run in a separate thread, we also off-loaded as many computational tasks as possible to avoid blocking the main JavaScript thread.

In the end, we built a React hook that any component can call with a widget definition. The hook passes the definition to the web worker, which adds it to the queue and generates the appropriate watermark. The only thing it returns to the calling component is the Base64 URL for the watermark bitmap. After all, that’s all the component needs.

Compressing large payloads

During extended testing, we noticed that some batch requests were quite large. For users quickly scrolling through a populated dashboard, the payloads could easily reach several hundred kilobytes and sometimes time out.

These batches are made up of similarly-shaped JSONs, sent from a web worker. They’re repetitive. And since we’re already inside a web worker, we can do some “cheap,” non-blocking computation.

It was time to do some gzipping in the browser.

We noticed that compressed requests were about 10x smaller than uncompressed ones, and the number of timeouts dropped by roughly 90%. We now compress any request that includes 10 or more widgets, about 35% of all requests. This avoids extra computation for small batches while ensuring large ones are compressed efficiently.

Deprioritizing off-screen content

The last optimization we made on the frontend is something we often did with widgets, both in the context of data fetching and rendering: deprioritizing off-screen content. We don’t render watermarks or cache widget contents if they are not visible, because they cannot be screenshotted anyway.

Supporting various color schemes

To keep the watermarks invisible, we want to use colors that are close to the base border color. This shouldn’t be hard, right? Dashboard widgets always use the same border color.

Ah—wait. There’s a dark theme. And high contrast, right. With different colors for both dark and light themes. That’s four different backgrounds so far?

Did you know notebooks use different colors, too? We do now, but we weren’t too happy when we found out.

Still, the frontend part is easy. We just pick a color based on the page and context. But how can the backend know what color to expect? In theory, we could explicitly support every possible color scheme, but that would require constant manual updates and would be fragile in practice, breaking anytime the frontend changes a theme or tweaks a color variable.

What if the backend just looks for any two pixels of the same color that are exactly 32 px apart? Those could be sentinel pixels. From there, we can infer the base color and check whether the pixels between them form a valid watermark for that color.

When you think about it, it’s just an extra short loop. Doesn’t add much complexity. But it adds a lot of flexibility.

We were pleasantly surprised to find out that even heavily tinted screenshots of widgets were still completely readable.

A Datadog widget displaying watermark resilience under heavy color tinting.
Datadog widget showing a tinted screenshot scenario.
A Datadog widget displaying watermark resilience under heavy color tinting.

Detecting watermarks through color tint is a cool trick, but it’s not that useful.

The real win is that the frontend can dynamically pick a color that blends in, and the backend will still detect the watermark. There’s no need to hardcode values for every specific context. That makes it possible to place watermarks in even more places, including UI elements that change color dynamically or unpredictably.

Possible applications

We can take this further.

Because screenshots now carry recoverable metadata, they don’t have to be static artifacts anymore. Today, you can paste a widget screenshot directly into a dashboard and reconstruct it as a fully functional visualization.

This opens up new ways to move between visual context and live data without changing how users share or capture information today.

In addition to reconstructing widgets from screenshots, there’s also that Slack integration we mentioned earlier. We could convert screenshots into practical sharing links and attach them to the messages that contain screenshots, just like we already do when you paste a share link and we add a snapshot.

This would make chat history a lot more useful, since sharing links live forever and watermark definitions expire after an hour.

Wrapping up

We built a system that embeds widget metadata invisibly inside screenshots—resilient to color profiles, DPI settings, and even copy-paste workflows.

Demo: Copying a Datadog widget from a screenshot using a hidden watermark.

It runs constantly, adds no visual noise, and can generate over a billion watermarks per day without slowing anything down. The result is a feature that’s nearly impossible to notice until the moment you need it.

If solving hard frontend performance problems at scale sounds like your kind of challenge, we’re hiring!