Skip to content

Upgrade Dynatrace RUM: script-tag loading, runtime config, fetch tooling - #425

Draft
redbugz wants to merge 34 commits into
frontierMasterfrom
dynatrace-rum-upgrade
Draft

Upgrade Dynatrace RUM: script-tag loading, runtime config, fetch tooling#425
redbugz wants to merge 34 commits into
frontierMasterfrom
dynatrace-rum-upgrade

Conversation

@redbugz

@redbugz redbugz commented Jul 29, 2026

Copy link
Copy Markdown

Summary

Upgrades Dynatrace RUM to the latest agent (build 10341260622154106) and loads it exclusively via <script> tags to fix an EJS 500, adds runtime config injection with baked-in fallbacks, and adds a script to automate future RUM upgrades. Ships as @fs/react-scripts 8.17.0.

Reasoned Changes

New RUM loaded exclusively via script tags (dynatrace.ejs)

What: Rewrote the three treatment branches so that when frontier_snow_dynatraceNewRUM is on, each mechanism emits a <script> tag (SRI async, SRI sync, or _complete.js async) instead of an EJS include() of the agent.
Why: The 1.341-era agent's minified JS contains the EJS open-delimiter (<%) inside a string, so include() made EJS 500 with "Could not find matching close tag" on every page. Loading only via script tags sidesteps the EJS compile step and avoids re-shipping 300–460 KB per view. asyncCS-inline was remapped to a synchronous SRI tag (name kept for old-RUM compatibility) since the new RUM has no small-bootstrap format.

Runtime config injection with baked-in fallbacks

What: Each new-RUM value reads locals.dynatrace.* first, falling back to publish-time constants; adds scripts/dynatrace-rum-config.json.
Why: RUM app IDs / integrity / data-dtconfig rotate Dynatrace-side with no way to refresh without a react-scripts release. This lets Snow supply fresh values at runtime (from the S3 config, consumed by fs-webdev/snow#446) while fallbacks keep older Snow deploys working. Two independent flags separate mechanism rollout from version rollout; with the version flag off, behavior is unchanged.

Release automation for future RUM upgrades (fetch-dynatrace-scripts.js)

What: Node script that calls the Dynatrace tag endpoints per env, rewrites the dynatrace.ejs fallbacks, writes the config JSON, and publishes it to S3.
Why: No repeatable path existed to consume new RUM releases. Fails fast with an aws sts get-caller-identity preflight + SSO-aware guidance, warns when AWS_PROFILE is unset, prints the downloaded agent version, and reads the token from the macOS keychain.

Release metadata and operator documentation

What: Version bump to 8.17.0, CHANGELOG-FRONTIER.md 8.17.0 entry, and scripts/DYNATRACE_RUM_UPDATE.md runbook.
Why: Ships as the @fs/react-scripts release Snow consumes. The runbook documents the non-obvious asyncCS-inline remap so future operators don't "fix" it back to an inline include.

Related PRs

  • fs-webdev/snow#446 (companion) — dynamic Dynatrace RUM config injection; consumes the dynatrace-rum-config.json this PR publishes and the locals.dynatrace.* values dynatrace.ejs reads.
  • fs-webdev/snow#447 — EJS 3 via ejs-mate 3.0.0; the EJS-3 upgrade the layout.ejs include conversion (merged from frontierMaster) targets.

Risk Assessment

Residual risk is server-side render behavior in dynatrace.ejs and the template-rewriting regex in the fetch script.

  1. fetch-dynatrace-scripts.js replaceFallback — matches the fallback body with {[^}]+}, stopping at the first }; a future data-dtconfig value with a literal/decoded } would truncate the rewrite. Values are URL-encoded today, so low-probability, but worth knowing.
  2. dynatrace.ejs asyncCS-inline (new) — emits a synchronous SRI <script>; if the baked-in integrity fallback ever drifts from the CDN asset and locals.dynatrace is absent (older Snow deploy), the browser blocks it under SRI and RUM stops silently. Mitigated by the locals.dynatrace override (snow#446).

Testing

  • Verified all 11 layout .ejs templates compile under EJS 3.1.10 (the version this must survive) via a standalone ejs.compile harness — the exact regression class behind the original 500.
  • Render-tested every treatment × new/old-flag branch: correct script tags emitted, locals.dynatrace.* overrides fallbacks, old-RUM paths unchanged.
  • fetch-dynatrace-scripts.js syntax-checked; AWS preflight fail-fast path exercised; ran end-to-end against Dynatrace to produce the committed config (agent 10341260622154106, published to S3).
  • Gap: no in-repo automated test harness for the EJS layer (root npm test is a no-op); verification was manual/scripted.

redbugz and others added 30 commits May 26, 2026 14:06
- Add new inline scripts for all three environments (int, beta, prod) with agent version 10337260504112723
- Update dynatrace.ejs with frontier_snow_dynatraceNewRUM feature flag for gradual rollout
- When new flag is ON: use 1.329+ version with enhanced data collection (owasp=1, uxrgce=1)
- When new flag is OFF: use existing RUM version (backward compatible)
- All three mechanisms supported: asyncCS-script, asyncCS-inline, global-cdn
- New CDN URLs use SRI integrity hashes for added security
- Existing frontier_snow_dynatraceRUM flag unchanged (still controls mechanism selection)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add fetch-dynatrace-scripts.js helper for automated future RUM updates
- Fetches latest scripts from Dynatrace API
- Auto-writes inline scripts to _inline_*_new.ejs files
- Displays CDN URLs and integrity hashes in copy-paste format
- Add DYNATRACE_RUM_UPDATE.md with complete update procedures
- Documents API setup, entity IDs, and testing procedures
- Enables maintainers to automate RUM upgrades in the future

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Report the active loading mechanism (asyncCS-script, asyncCS-inline, or global-cdn) as a custom metric to Dynatrace. This enables direct DQL queries to correlate loading mechanism with Core Web Vitals performance (LCP, CLS, page load time).

- Add window.dtrum.reportCustomMetric call to emit rum_loading_mechanism value
- Bump version to 8.17.0-alpha.1 for telemetry feature

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The RUM agent needs the data-dtconfig attribute (which includes reportUrl) to send beacons directly to the Dynatrace tenant instead of back through the app. Previously, only src and integrity were being extracted from the Dynatrace API response.

Changes:
- Extract and preserve data-dtconfig from complete script tags
- Add cdnConfigNew object in dynatrace.ejs with per-environment configs
- Apply data-dtconfig attribute to asyncCS-script and global-cdn mechanisms
- Print config extraction confirmation in fetch script

This fixes the 405 POST errors by ensuring RUM beacons are routed to the correct Dynatrace backend endpoint (reportUrl), not to the app.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Includes full data-dtconfig with proper reportUrl for direct beacon delivery to Dynatrace tenant.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The new RUM script expects config in JSON format (data-config, data-appconfig, data-envconfig) which the fetch script provides in the inline script files. External script tags cannot reliably read data-* attributes, so we use the inline approach for both asyncCS-script and global-cdn mechanisms when new RUM is enabled.

This fixes the initialization error: 'DT_RUM will not initialize: None of the supported configuration sources contain the required properties [revision, environmentId, app]'

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…ease

- Reflect version bump to 8.17.0-alpha.2 in package-lock.json
- Make fetch-dynatrace-scripts.js executable

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Fixes RUM initialization by using inline script approach with proper JSON config format for new RUM.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Report rum_loading_mechanism metric immediately after each RUM script loads, rather than at the end of page. This ensures the custom metric is captured in the first page summary event before any beacons are sent.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Fixes telemetry timing to report RUM loading mechanism before first page summary beacon.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
asyncCS-script should load the script asynchronously to not block page rendering. Add missing async attribute to old RUM Edge CDN script.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Update fetch-dynatrace-scripts.js to call /api/v2/rum/inlineCode endpoint for actual JavaScript code
- Write actual minified inline code to _inline_*_new.ejs files (300-460KB) instead of script tags
- Fix dynatrace.ejs to properly differentiate the three mechanisms:
  * asyncCS-script: async external from CDN (Edge for old, Dynatrace for new)
  * asyncCS-inline: full inline embedding (300-460KB for new, ~35KB for old)
  * global-cdn: synchronous external from Dynatrace CDN
- Each mechanism now clearly uses distinct loading patterns for telemetry differentiation

This enables accurate performance comparison of mechanisms via Dynatrace telemetry.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Fetch and display:
- /api/v2/rum/javaScriptTag (simple tag, 157 chars)
- /api/v2/rum/oneAgentJavaScriptTagWithSri (SRI tag, ~1KB)
- /api/v2/rum/inlineCode (inline code, 300-460KB)

Shows both CDN URLs for comparison: basic variant vs new SRI variant.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Extract env values into separate helper function for reusability
- Update dynatrace.ejs with per-env fallback pattern using locals.dynatrace
- Generate dynatrace-rum-config.json for Snow to publish to CDN
- Snow can inject fresh values at runtime via locals.dynatrace override
- Clearer next steps in console output

This enables decoupling: react-scripts has built-in fallback values, but
Snow can override with latest values from dynatrace-rum-config.json CDN file.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Update dynatrace.ejs to support Snow providing fresh config values via
res.locals.dynatrace, with fallback to hardcoded values. This enables:

- Old Snow: transparently uses hardcoded fallback values
- New Snow: overrides with fresh values from CDN config file
- Old react-scripts + new Snow: new RUM works with stale but functional config
- New react-scripts + old Snow: new RUM works with publish-time config

For asyncCS-inline, also check for Snow-provided inline script before
falling back to included file.

This completes the progressive enhancement architecture where react-scripts
carries current values at publish time, and Snow can override them dynamically
without breaking older app/runtime combinations.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Script now publishes dynatrace-rum-config.json to S3 after updating local files.
Uses AWS CLI (aws s3 cp) instead of SDK, which:
- Leverages existing AWS credential infrastructure (~/.aws/credentials or env vars)
- No additional Node dependencies needed
- Simpler setup: just 'aws configure' or set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
- Works with standard AWS credential chain (no custom keychain storage)

Configuration:
- S3_PUBLISH_BUCKET: S3 bucket for config file (default: frontier-rum-config)
- S3_PUBLISH_REGION: AWS region (default: us-east-1)
- Both can be overridden via environment variables

Keychain setup now only stores Dynatrace API token (AWS uses standard auth).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Simplify the configuration property names published to S3 and used by Snow:
- cdnUrlsNew → cdnUrls
- cdnIntegrityNew → cdnIntegrity
- cdnConfigNew → cdnConfig
- inlineScriptNew → inlineScript

Updates:
- fetch-dynatrace-scripts.js: generate cleaner property names
- dynatrace.ejs: read from locals.dynatrace with cleaner keys
- dynatrace-rum-config.json: updated config structure

The 'New' postfix was redundant since these are already scoped to the new RUM.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Snow servers and other services can now fetch dynatrace-rum-config.json
without requiring AWS credentials or special access.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…ents

Convert template literals to string concatenation in include() calls for
asyncCS-inline, asyncCS-script, and global-cdn fallback branches. EJS
parser cannot handle backticks inside template tags.

Also consolidate reporting mechanism calls to reduce duplication.
EJS requires <%- %> (with hyphen) to output included file content.
Control-flow syntax <% %> cannot execute include() function calls.
Revert to <% include(...) %> syntax without hyphen, matching the pattern
in layout.ejs. Remove leading './' from path since include() resolves
relative to views directory.
EJS include directive requires a static identifier or variable, not
inline function calls. Pre-compute dynamic paths using string concatenation,
then pass the variable to include for proper resolution.
…ents

Convert template literals to string concatenation in include() calls for
asyncCS-inline, asyncCS-script, and global-cdn fallback branches. EJS
parser cannot handle backticks inside template tags. Use <%- include() %>
with string concatenation to support dynamic path calculation.
Log which treatment and code path is taken during dynatrace initialization
to confirm we're running the latest deployed version.
Replace optional chaining (?.) with standard property access checks.
Pre-compute include paths as variables to avoid string concatenation
in include() calls, which may confuse EJS parser.
The new RUM agent's minified JS contains the "<%" sequence, which EJS
include() misreads as an unterminated scriptlet ("Could not find matching
close tag"), causing a 500. Load the new agent exclusively via <script>
tags (SRI tag or _complete.js) instead of inlining, and remap each
treatment to a Dynatrace snippet format:
  asyncCS-script -> OneAgent JS tag + SRI, async
  asyncCS-inline -> OneAgent JS tag + SRI, sync (no longer inlines)
  global-cdn     -> _complete.js, async

- Rewrite fetch-dynatrace-scripts.js: drop /inlineCode fetch and
  _inline_*_new.ejs generation; add cdnCompleteUrls; robust fallback
  rewriter that preserves the locals-first progressive-enhancement guard
- Delete generated _inline_*_new.ejs; old-RUM _inline_*.ejs untouched
- Refresh CDN config to agent 10337260526121002; update docs and changelog

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…um-upgrade

# Conflicts:
#	CHANGELOG-FRONTIER.md
#	package-lock.json
#	packages/react-scripts/package.json
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
redbugz and others added 4 commits July 29, 2026 06:56
…n fix

- Preflight 'aws sts get-caller-identity' before any Dynatrace work; warn when
  AWS_PROFILE is unset and print SSO-aware setup guidance on failure (fail fast)
- Rely on the standard AWS_PROFILE / credential chain (aws children inherit env)
- Print the downloaded RUM agent version at the end of the run
- Align KEYCHAIN_SERVICE_DYNATRACE with the stored keychain item + fix setup doc

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consume the latest RUM release via fetch-dynatrace-scripts.js: refresh the
dynatrace.ejs publish-time fallbacks and dynatrace-rum-config.json (also
republished to S3). Supersedes agent 10337260526121002.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drop the per-render '[dynatrace.ejs] Loaded' server log (redundant with the
  existing sendSessionProperties RUM telemetry; avoids prod log spam)
- Sync package-lock.json self-version to 8.17.0 to match package.json (final release)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@redbugz
redbugz marked this pull request as draft July 29, 2026 16:28
@redbugz
redbugz requested a review from Copilot July 29, 2026 16:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR upgrades Dynatrace RUM to a newer agent version and switches new-RUM loading to <script> tags (avoiding EJS include() compilation failures), adds runtime-config fallbacks via locals.dynatrace.*, and introduces tooling/docs to automate future RUM updates and publishing of a config artifact.

Changes:

  • Reworked dynatrace.ejs to support dual-flag rollout (mechanism vs version) and load new RUM exclusively via script tags with SRI/config support.
  • Added a fetch/publish automation script plus a generated dynatrace-rum-config.json artifact for Snow runtime injection.
  • Bumped @fs/react-scripts to 8.17.0 and documented/released the change.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/react-scripts/layout/views/partials/dynatrace.ejs Adds new-RUM script-tag branches, locals-first runtime config, and dual-flag gating.
packages/react-scripts/scripts/fetch-dynatrace-scripts.js New automation script to fetch Dynatrace tags, update EJS fallbacks, generate config JSON, and publish to S3.
packages/react-scripts/scripts/dynatrace-rum-config.json Generated per-env CDN URL/SRI/config values for Snow to consume at runtime.
packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md Operator runbook for fetching/publishing and validating treatment mappings.
packages/react-scripts/package.json Version bump to 8.17.0.
package-lock.json Lockfile version update for the react-scripts package entry.
CHANGELOG-FRONTIER.md Release notes for 8.17.0 Dynatrace RUM changes.
Comments suppressed due to low confidence (3)

packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md:81

  • This relative link points to ./partials/dynatrace.ejs, but the actual file lives under layout/views/partials/dynatrace.ejs. As written, the link is broken.
Two flags drive the experiment (see [dynatrace.ejs](./partials/dynatrace.ejs)):

packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md:184

  • These relative links are incorrect from the scripts/ directory. dynatrace.ejs is under layout/views/partials/, and fetch-dynatrace-scripts.js is the current file (same directory), so the links should be updated to avoid 404s in the runbook.
- [dynatrace.ejs](./partials/dynatrace.ejs) - Main RUM configuration file
- [fetch-dynatrace-scripts.js](./fetch-dynatrace-scripts.js) - The fetch script itself

packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md:73

  • The example snippet treats cdnIntegrityNew as a single string, but dynatrace.ejs defines cdnIntegrityNew as an environment-keyed object (and integrity values may differ per env). The example should reflect the actual shape.
```javascript
const cdnUrlsNew = {
  int: '...',    // Copy from script output
  beta: '...',   // Copy from script output
  prod: '...'    // Copy from script output

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +65
### Step 2: Update CDN URLs in dynatrace.ejs

The script output will display CDN URLs in this format:

```
CDN URLs for dynatrace.ejs (cdnUrlsNew object):

int: 'https://js-cdn.dynatrace.com/...',
beta: 'https://js-cdn.dynatrace.com/...',
prod: 'https://js-cdn.dynatrace.com/...',

Integrity hash for all environments:
cdnIntegrityNew = 'sha256-...'
```
Comment on lines +93 to +97
<script type="text/javascript">
if (typeof window !== 'undefined' && window.dtrum) {
window.dtrum.sendSessionProperties({rum_loading_mechanism: "<%= dynatraceFlag.treatment %>"});
}
</script>
Comment on lines +269 to +287
async function publishToS3(config) {
try {
const configJson = JSON.stringify(config);
const tempFile = path.join(__dirname, '.dynatrace-rum-config-temp.json');
fs.writeFileSync(tempFile, configJson, 'utf8');

// Use AWS CLI for S3 upload (inherits this process's env → honors AWS_PROFILE / default chain)
const awsCmd = `aws s3 cp "${tempFile}" s3://${S3_PUBLISH_BUCKET}/dynatrace-rum-config.json --region ${S3_PUBLISH_REGION} --metadata "generated=$(date +%s)" --cache-control "max-age=300" --acl public-read`;

execSync(awsCmd, { stdio: 'inherit' });
fs.unlinkSync(tempFile);

console.log(` ✅ Published dynatrace-rum-config.json to S3 (s3://${S3_PUBLISH_BUCKET}/dynatrace-rum-config.json)`);
} catch (error) {
console.error(` ❌ Failed to publish to S3: ${error.message}`);
printAwsSetupInstructions();
throw error;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants