Compatibility

Does Obfuscation Break Server-Sent Events and Log Streams?

A streaming feed is a contract you are on the receiving end of and cannot renegotiate. Each line of NDJSON, each frame of a server-sent-event stream, carries field names chosen by whatever produced it. Your code reads those names. Nothing in your build writes them, which is exactly the arrangement that member renaming handles worst -- and monitoring code is the last place anyone looks when something is wrong.

What was measured

One sample holding two payloads as text, exactly as they arrive on a wire. The first is a newline-delimited JSON log feed of three records, with the fields a service emits: timestamp, level, service name, message, an order identifier and an amount. The second is a server-sent-event stream framed the way the specification puts it on the wire, with event, id and data lines separated by blank lines.

The sample parses both, reads fields, filters for errors, groups amounts by service, evaluates an alerting rule, dispatches on the event name and reads the payload nested inside the data field. It ends by emitting a summary record of its own, which is the only object on the page whose names the file itself authors.

The base column is clean: all five protection profiles reproduced the unprotected output line for line. If you are protecting a build that consumes a feed, the parsing itself is not a risk. Everything below needs member renaming switched on and a pattern that reaches the sender's field names.

An error stream that reports no errors

The first arm is the one that should worry anyone who runs a service. With a pattern matching the level, service and message fields, the error count went from one to zero, the error message printed as a placeholder, and the alerting rule went from firing to not firing.

Nothing threw. The filter asks each record for a level, gets undefined, compares it against the string that marks an error, and gets false for every record in the feed. A filter that matches nothing returns an empty array, an empty array has a length of zero, and zero errors is a perfectly ordinary thing for a healthy system to report. The stream still contains the error. The code that exists to notice it cannot.

This is the same shape as a security check that fails open, and it deserves the same reaction. The failure has no signal of its own: no exception, no dropped connection, no gap in a graph. The only evidence is a negative -- an alert that stopped arriving -- and negatives are noticed late, usually by a customer.

Grouped totals collapse into a single bucket named undefined

The aggregation arm produces a line that is hard to misread once you have seen it. Grouping amounts by service name went from two named buckets with correct totals to a single bucket literally keyed undefined, holding the sum of everything.

The mechanism is worth following. The grouping key is read from each record, so it comes back undefined. Using it as a property name converts it to the string undefined, so every record lands in the same bucket. One service's rows and another's are added together, and the second service disappears from the output entirely.

A second arm renames the amount field instead and gives the other half of the picture: the buckets keep their correct names and every total serialises as null. That is the ordinary rule that a numeric read of a missing property produces NaN, and that NaN is written as null when the object is serialised. A dashboard shows the right services with no values, which reads like a collection outage rather than a build change.

The event parser discards every frame, then fails on the first read

Server-sent events have a two-stage shape that turns one renamed name into two different failures in a single run. The parser splits each line at the first colon and stores the value under the field name it just read, so those keys come from the text of the stream. The completion test, however, is a property read -- does this frame have a data field yet -- and that read is a rename site.

With a pattern matching the event, id and data field names, the completion test always read undefined, so no frame was ever considered complete and the parsed event count went from two to zero. The stream arrived intact and was thrown away frame by frame, quietly.

The loud part comes immediately afterwards. Code that reads the first event of an empty list gets undefined, and the next property read throws a type error naming a generated identifier. So the visible symptom is a crash on a line that looks unrelated, several steps after the actual problem, with a name in the message that appears nowhere in the source. If you see a generated name in an error while reading a stream, look at the frame-completion test rather than the line that threw.

Reading the payload inside the data field behaves like every other parsed document. A pattern matching the quote fields left the price read as undefined and the computed spread as NaN, with nothing thrown at all.

The feed is intact in every arm, which is where the confusion starts

Two lines never moved in any measurement, and they are the two a first investigation prints. The number of records parsed out of the log feed stayed correct, and the list of field names on the first record stayed correct -- the sender's names, exactly as sent.

So the evidence available at the top of an investigation says the pipeline is healthy: the connection is up, the records are arriving, the fields are present, and the parser is producing objects with the right shape. Everything downstream of that -- the filter, the grouping, the alert -- is quietly answering questions about properties nobody is asking for.

It is worth being precise about where the boundary falls, because the sender's names being intact is what makes the failure survivable once you know about it. The data never changes. The stream never changes. Only the spelling used to reach into it changes, and only inside the protected build. Reverting the member pattern restores every reader without touching a byte of stored data, which is not true of the storage-side failures in this family.

The one object you own moves without anything noticing

The summary record the sample emits at the end is authored entirely in the file, so a pattern matching its field names renames both the write and the read, and everything inside the program stays consistent. The only thing that changed was the document that leaves: its keys became generated names.

That is the output side of the contract, and it has no in-process signal whatsoever. Every test that reads the summary back reads it through the same renamed names and agrees with itself. The system that cannot read it is the one downstream -- a collector, an index, a rule that keys off a field name -- and it will describe the problem as missing data rather than as changed data.

The rule that separates the two halves is not about the option, it is about which side of the boundary the renamed spelling sits on. If your source both writes and reads a name, renaming is invisible inside the program and visible only outside it. If something else writes it and your source only reads it, the value is gone and the program carries on with undefined.

What to check, and how to keep a pattern away from a feed

The cheapest check is a single field with a known value. Take one record out of the feed, print one field, and compare it against a literal you typed. Do not compare two reads of the same record against each other: under renaming both sides return undefined and the comparison passes while both are wrong.

For the aggregation layer, print the bucket names rather than the totals. A bucket named undefined is unmistakable, appears immediately, and cannot be confused with a quiet day. For the event parser, print the parsed frame count before anything consumes it, since zero frames from a stream that is plainly arriving is the clearest signal on this page.

The structural fix is the same one that applies to every external document: anchor the member pattern to names your own code writes and reads, and keep it clear of any field name chosen by a sender. Logging and telemetry code deserves particular care, because it is the part of a system that is least exercised by tests and most trusted during an incident.

Frequently asked questions

Does obfuscation break server-sent events?

Not in the default configuration. A sample parsing a real server-sent-event stream and a newline-delimited JSON log feed produced identical output on all five protection profiles measured. Streams become a surface only when member renaming is switched on and the pattern matches field names written by the sender.

Why did my error alerting stop firing after protecting the build?

Because the filter that identifies errors reads a field the sender wrote. Renaming that read returns undefined, the comparison against the error level is false for every record, and a filter that matches nothing produces an empty list. The measured error count went from one to zero and the alerting rule went from true to false, with nothing thrown.

Why is my grouped chart showing one bucket called undefined?

Because the grouping key was read through a renamed property. The read yields undefined, using it as a property name converts it to the string undefined, and every record lands in that one bucket. In the measured run two correctly named buckets became a single bucket holding the combined total, with one service absent from the output.

Why do my totals arrive as null after obfuscation?

Because a numeric read of a missing property produces NaN and NaN is serialised as null. The measured aggregation kept its correct bucket names and reported null for every total. On a dashboard that looks like a collection outage rather than a change in the build, which is what makes it expensive to diagnose.

Why does my event stream parser discard every frame?

Because the frame-completion test is a property read while the field names themselves come from the text of the stream. Renaming the read makes the test always see undefined, so no frame is ever considered complete. The measured parsed event count went from two to zero, and the first read of the empty result then threw a type error naming a generated identifier.

Are the field names in my own summary records affected?

Yes, but only outside the program. When your source both writes and reads a name, renaming rewrites both halves and everything inside the process stays consistent, so tests pass. The emitted document carries generated keys, and the only system that can see that is the collector or index downstream, which will report it as missing data.

How do I check a feed consumer in a protected build?

Print one field from one record and compare it against a literal you typed, never against another read of the same record, because two renamed reads agree with each other while both are wrong. Then print the bucket names from your aggregation and the parsed frame count. A bucket named undefined or a frame count of zero is unambiguous.

Related reading