Real cases · 22 of 25
A one-line change to an inherited page property's default value quietly changed behavior on hundreds of pages that had never explicitly set it — how it was traced, and the audit query that should have run before the change shipped.
Background reading: Inherited Page Properties and Site-Wide Feature Toggles covers the mechanics this post assumes.
A marketing requirement said analytics cookies should be enabled by default across most of the site, with only a small, known set of legacy pages needing them left off. The change that shipped looked trivially small: one boolean literal, in one Java file, changed from false to true — the hardcoded default passed to getInherited(ANALYTICS_COOKIES_ENABLED, false) became getInherited(ANALYTICS_COOKIES_ENABLED, true).
Within a day, compliance flagged that analytics cookies were now firing on pages that were never supposed to have them — a handful of region-specific pages under strict local privacy requirements. Those pages had never explicitly set the property either way; they had simply relied on the old site-wide default of false, the same as almost every other page on the site. Nobody had a list of which pages fell into that category, because "relying on the default" isn't something that shows up anywhere in content — it's the absence of a property, indistinguishable in the repository from a page that simply never needed to think about the setting at all.
The fix needed to happen in two places, because the actual bug was a missing step in the rollout process, not a bug in the inheritance code itself:
analyticsCookiesEnabled to false at the section roots for the affected region-specific pages, so they no longer depend on (and are no longer affected by) the site-wide default at all — regardless of what that default is ever changed to in the future.Explicitly setting the property on the affected pages, rather than reverting the default back to false globally, was the deliberate choice — reverting would have undone the actual goal (most of the site should default to enabled) just to patch the pages that needed an exception. The correct fix scopes the exception to where it belongs.
class InheritedTogglePreflightTest {
@Test
void defaultChangeIsAuditedAgainstPagesRelyingOnIt() throws Exception {
List<PageCandidate> allPages = pageTreeScanner.scanAll("/content/mysite");
List<PageCandidate> pagesRelyingOnDefault = allPages.stream()
.filter(page -> !page.hasExplicitProperty("analyticsCookiesEnabled"))
.collect(Collectors.toList());
List<PageCandidate> complianceSensitivePages = pagesRelyingOnDefault.stream()
.filter(page -> page.getPath().matches(".*/(eu|region-[a-z]{2})/.*"))
.collect(Collectors.toList());
assertThat(complianceSensitivePages)
.as("compliance-sensitive pages must never rely on an inherited toggle's default - "
+ "they should set the property explicitly regardless of what the default is")
.isEmpty();
}
@Test
void explicitFalseIsPreservedRegardlessOfDefaultChange() {
Resource pageWithExplicitFalse = context.create().resource(
"/content/mysite/eu/privacy-page/jcr:content",
Map.of("analyticsCookiesEnabled", false));
InheritanceValueMap inherited = new HierarchyNodeInheritanceValueMap(pageWithExplicitFalse);
// Regardless of what default is passed here, an explicit false on this
// exact resource always wins - this assertion should hold both before
// and after any change to the hardcoded default elsewhere in the code.
assertThat(inherited.getInherited("analyticsCookiesEnabled", true)).isFalse();
}
}
The first test is the preflight check that should run before any hardcoded default changes — it fails loudly, listing exactly which compliance-sensitive pages are exposed, rather than discovering the exposure after the fact via a compliance complaint.
Remediation — explicit property set at the affected section roots, rather than relying on any future default:
<!-- /content/mysite/eu/jcr:content -->
<jcr:content
analyticsCookiesEnabled="{Boolean}false"
... />
Audit tooling — a scanner used both for the incident remediation and wired into the preflight test above:
final class PageTreeScanner {
private final ResourceResolver resourceResolver;
PageTreeScanner(ResourceResolver resourceResolver) {
this.resourceResolver = resourceResolver;
}
List<PageCandidate> scanAll(String rootPath) {
Resource root = resourceResolver.getResource(rootPath);
List<PageCandidate> results = new ArrayList<>();
if (root != null) {
collect(root, results);
}
return results;
}
private void collect(Resource resource, List<PageCandidate> results) {
Resource contentResource = resource.getChild(JcrConstants.JCR_CONTENT);
if (contentResource != null) {
results.add(new PageCandidate(resource.getPath(), contentResource.getValueMap()));
}
for (Resource child : resource.getChildren()) {
collect(child, results);
}
}
}
final class PageCandidate {
private final String path;
private final ValueMap ownProperties;
PageCandidate(String path, ValueMap ownProperties) {
this.path = path;
this.ownProperties = ownProperties;
}
String getPath() {
return path;
}
boolean hasExplicitProperty(String name) {
return ownProperties.containsKey(name);
}
}
The key distinction this scanner makes deliberately: ownProperties.containsKey(name) checks only the page's own jcr:content, never walking up — the opposite of HierarchyNodeInheritanceValueMap, and exactly the check needed to answer "does this specific page rely on the default" rather than "what value does this page effectively have."
analyticsCookiesEnabled="{Boolean}false" explicitly on each.true default.The remediation made the previously-implicit exception explicit in content, which means the six affected pages are now immune to any future default change, not just this one — nobody has to remember they were an edge case. The preflight test encodes the actual lesson (compliance-sensitive pages must never depend on an inherited default) as a repeatable, automated check rather than a note in a postmortem document that only helps if someone remembers to reread it before the next toggle change.
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.