Skip to content

Commit 3b16cdf

Browse files
Sugandhgylclaude
andauthored
feat: add rumConfig to Site config for deterministic RUM availability signal (#1596)
Adds rumConfig sub-config to the Site config object with hasDomainKey and lastCheckedAt fields, following the existing fetchConfig/onboardConfig pattern. Includes Joi schema, getRumConfig/hasRumDomainKey/updateRumConfig accessors, toDynamoItem serialization, and full unit test coverage. Please ensure your pull request adheres to the following guidelines: - [ ] make sure to link the related issues in this description - [ ] when merging / squashing, make sure the fixed issue references are visible in the commits, for easy compilation of release notes ## Related Issues Thanks for contributing! --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fb4d5f9 commit 3b16cdf

3 files changed

Lines changed: 178 additions & 0 deletions

File tree

packages/spacecat-shared-data-access/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,25 @@ The `deliveryConfig` object on a Site stores delivery infrastructure details. It
172172
| `preferContentApi` | boolean | Whether to prefer the Content API for content retrieval |
173173
| `contentSourcePath` | string | AEM content root path for a site. Used to disambiguate multiple sites that share the same Cloud Manager program and environment. Corresponds to `/content/<site-name>` in the AEM repository. |
174174

175+
## Site rumConfig
176+
177+
The `rumConfig` object on a Site tracks Real User Monitoring (RUM) domain key availability. It is set automatically on site creation and refreshed weekly by the `rum-config-refresh` audit handler.
178+
179+
| Property | Type | Description |
180+
|----------|------|-------------|
181+
| `hasDomainKey` | boolean | Whether the site's domain has an active RUM domain key registered |
182+
| `lastCheckedAt` | string (ISO 8601) | Timestamp of the most recent RUM domain key check |
183+
184+
### Config model methods
185+
186+
| Method | Returns | Description |
187+
|--------|---------|-------------|
188+
| `getRumConfig()` | `{ hasDomainKey, lastCheckedAt } \| undefined` | Returns the current rumConfig, or `undefined` if not yet set |
189+
| `hasRumDomainKey()` | `boolean` | Returns `true` if a RUM domain key is active, `false` otherwise |
190+
| `updateRumConfig(hasDomainKey)` | `void` | Sets `hasDomainKey` and updates `lastCheckedAt` to the current time |
191+
192+
Sites created before the `rum-config-refresh` handler ran will have no `rumConfig` key — callers must treat `undefined` as "unknown" rather than "not set up".
193+
175194
## Architecture
176195
177196
```

packages/spacecat-shared-data-access/src/models/site/config.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,10 @@ export const configSchema = Joi.object({
407407
startTime: Joi.number().optional(),
408408
})).optional(),
409409
}).optional(),
410+
rumConfig: Joi.object({
411+
hasDomainKey: Joi.boolean().required(),
412+
lastCheckedAt: Joi.string().isoDate().required(),
413+
}).optional(),
410414
commerceLlmoConfig: Joi.object().pattern(
411415
Joi.string(),
412416
Joi.object({
@@ -542,6 +546,18 @@ export const Config = (data = {}) => {
542546
self.getEdgeOptimizeConfig = () => state?.edgeOptimizeConfig;
543547
self.getOnboardConfig = () => state?.onboardConfig;
544548
self.getCommerceLlmoConfig = () => state?.commerceLlmoConfig;
549+
/**
550+
* Returns the RUM configuration for the site, or undefined if not set.
551+
* Returns a shallow copy to prevent callers from mutating internal state.
552+
* @returns {{ hasDomainKey: boolean, lastCheckedAt: string } | undefined}
553+
*/
554+
self.getRumConfig = () => (state?.rumConfig ? { ...state.rumConfig } : undefined);
555+
556+
/**
557+
* Returns true if RUM data collection is confirmed active for this site.
558+
* @returns {boolean}
559+
*/
560+
self.hasRumDomainKey = () => state?.rumConfig?.hasDomainKey === true;
545561
const AUDIT_TARGET_SOURCES = ['manual', 'moneyPages'];
546562
const auditTargetEntrySchema = Joi.object({
547563
url: Joi.string().uri().required(),
@@ -955,6 +971,22 @@ export const Config = (data = {}) => {
955971
state.commerceLlmoConfig = commerceLlmoConfig;
956972
};
957973

974+
/**
975+
* Records the outcome of a RUM domain-key check and updates the timestamp.
976+
* @param {boolean} hasDomainKey - Whether the site has an active RUM domain key.
977+
* @param {Date} [now=new Date()] - Timestamp for lastCheckedAt; injectable for tests.
978+
* @throws {Error} if hasDomainKey is not a boolean.
979+
*/
980+
self.updateRumConfig = (hasDomainKey, now = new Date()) => {
981+
if (typeof hasDomainKey !== 'boolean') {
982+
throw new TypeError(`updateRumConfig: hasDomainKey must be a boolean, got ${typeof hasDomainKey}`);
983+
}
984+
state.rumConfig = {
985+
hasDomainKey,
986+
lastCheckedAt: now.toISOString(),
987+
};
988+
};
989+
958990
return Object.freeze(self);
959991
};
960992

@@ -974,6 +1006,7 @@ Config.toDynamoItem = (config) => ({
9741006
edgeOptimizeConfig: config.getEdgeOptimizeConfig(),
9751007
onboardConfig: config.getOnboardConfig?.(),
9761008
commerceLlmoConfig: config.getCommerceLlmoConfig?.(),
1009+
rumConfig: config.getRumConfig?.(),
9771010
enableMoneyPageUrls: config.isMoneyPageUrlsEnabled?.() === false ? false : undefined,
9781011
auditTargetURLs: config.getAuditTargetURLsConfig?.(),
9791012
});

packages/spacecat-shared-data-access/test/unit/models/site/config.test.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3013,6 +3013,132 @@ describe('Config Tests', () => {
30133013
});
30143014
});
30153015

3016+
describe('rumConfig', () => {
3017+
describe('getRumConfig', () => {
3018+
it('returns rumConfig when set', () => {
3019+
const config = Config({
3020+
rumConfig: { hasDomainKey: true, lastCheckedAt: '2026-05-08T00:00:00.000Z' },
3021+
});
3022+
expect(config.getRumConfig()).to.deep.equal({
3023+
hasDomainKey: true,
3024+
lastCheckedAt: '2026-05-08T00:00:00.000Z',
3025+
});
3026+
});
3027+
3028+
it('returns undefined when rumConfig is absent', () => {
3029+
const config = Config({});
3030+
expect(config.getRumConfig()).to.be.undefined;
3031+
});
3032+
3033+
it('returns a copy so mutating the result does not affect internal state', () => {
3034+
const config = Config({
3035+
rumConfig: { hasDomainKey: true, lastCheckedAt: '2026-05-08T00:00:00.000Z' },
3036+
});
3037+
const copy = config.getRumConfig();
3038+
copy.hasDomainKey = false;
3039+
expect(config.hasRumDomainKey()).to.be.true;
3040+
});
3041+
});
3042+
3043+
describe('hasRumDomainKey', () => {
3044+
it('returns true when hasDomainKey is true', () => {
3045+
const config = Config({
3046+
rumConfig: { hasDomainKey: true, lastCheckedAt: '2026-05-08T00:00:00.000Z' },
3047+
});
3048+
expect(config.hasRumDomainKey()).to.be.true;
3049+
});
3050+
3051+
it('returns false when hasDomainKey is false', () => {
3052+
const config = Config({
3053+
rumConfig: { hasDomainKey: false, lastCheckedAt: '2026-05-08T00:00:00.000Z' },
3054+
});
3055+
expect(config.hasRumDomainKey()).to.be.false;
3056+
});
3057+
3058+
it('returns false when rumConfig is absent', () => {
3059+
const config = Config({});
3060+
expect(config.hasRumDomainKey()).to.be.false;
3061+
});
3062+
});
3063+
3064+
describe('updateRumConfig', () => {
3065+
it('sets hasDomainKey to true and records lastCheckedAt', () => {
3066+
const now = new Date('2026-05-08T12:00:00.000Z');
3067+
const config = Config({});
3068+
config.updateRumConfig(true, now);
3069+
const rum = config.getRumConfig();
3070+
expect(rum.hasDomainKey).to.be.true;
3071+
expect(rum.lastCheckedAt).to.equal('2026-05-08T12:00:00.000Z');
3072+
});
3073+
3074+
it('sets hasDomainKey to false and records lastCheckedAt', () => {
3075+
const now = new Date('2026-05-08T12:00:00.000Z');
3076+
const config = Config({});
3077+
config.updateRumConfig(false, now);
3078+
expect(config.getRumConfig().hasDomainKey).to.be.false;
3079+
expect(config.getRumConfig().lastCheckedAt).to.equal('2026-05-08T12:00:00.000Z');
3080+
});
3081+
3082+
it('overwrites a previous rumConfig value', () => {
3083+
const now = new Date('2026-05-08T12:00:00.000Z');
3084+
const config = Config({
3085+
rumConfig: { hasDomainKey: true, lastCheckedAt: '2025-01-01T00:00:00.000Z' },
3086+
});
3087+
config.updateRumConfig(false, now);
3088+
expect(config.hasRumDomainKey()).to.be.false;
3089+
expect(config.getRumConfig().lastCheckedAt).to.equal('2026-05-08T12:00:00.000Z');
3090+
});
3091+
3092+
it('throws TypeError when hasDomainKey is not a boolean', () => {
3093+
const config = Config({});
3094+
expect(() => config.updateRumConfig(null)).to.throw(TypeError, /hasDomainKey must be a boolean/);
3095+
expect(() => config.updateRumConfig(undefined)).to.throw(TypeError, /hasDomainKey must be a boolean/);
3096+
expect(() => config.updateRumConfig('true')).to.throw(TypeError, /hasDomainKey must be a boolean/);
3097+
expect(() => config.updateRumConfig(1)).to.throw(TypeError, /hasDomainKey must be a boolean/);
3098+
});
3099+
});
3100+
3101+
describe('Joi schema validation', () => {
3102+
it('accepts a valid rumConfig', () => {
3103+
expect(() => Config({
3104+
rumConfig: { hasDomainKey: true, lastCheckedAt: '2026-05-08T00:00:00.000Z' },
3105+
})).to.not.throw();
3106+
});
3107+
3108+
it('rejects rumConfig missing hasDomainKey', () => {
3109+
expect(() => validateConfiguration({
3110+
rumConfig: { lastCheckedAt: '2026-05-08T00:00:00.000Z' },
3111+
})).to.throw(/hasDomainKey/);
3112+
});
3113+
3114+
it('rejects rumConfig with invalid lastCheckedAt', () => {
3115+
expect(() => validateConfiguration({
3116+
rumConfig: { hasDomainKey: true, lastCheckedAt: 'not-a-date' },
3117+
})).to.throw(/lastCheckedAt/);
3118+
});
3119+
3120+
it('treats absent rumConfig as valid (optional field)', () => {
3121+
expect(() => validateConfiguration({})).to.not.throw();
3122+
});
3123+
});
3124+
3125+
describe('toDynamoItem serialization', () => {
3126+
it('includes rumConfig when set', () => {
3127+
const config = Config({
3128+
rumConfig: { hasDomainKey: true, lastCheckedAt: '2026-05-08T00:00:00.000Z' },
3129+
});
3130+
const item = Config.toDynamoItem(config);
3131+
expect(item.rumConfig).to.deep.equal(config.getRumConfig());
3132+
});
3133+
3134+
it('omits rumConfig from toDynamoItem when not set', () => {
3135+
const config = Config({});
3136+
const item = Config.toDynamoItem(config);
3137+
expect(item.rumConfig).to.be.undefined;
3138+
});
3139+
});
3140+
});
3141+
30163142
describe('LLMO Well Known Tags', () => {
30173143
const { extractWellKnownTags } = Config();
30183144

0 commit comments

Comments
 (0)