From e6cb3ecde9ac1f05eea9fdf5cba6c9c3e009a169 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 21 Jul 2026 22:53:47 -0600 Subject: [PATCH 1/2] fix(node): remove browser SDK dependencies --- packages/core/package.json | 1 - .../core/src/configuration/configuration.ts | 2 +- .../src/configuration/exposureEvent.types.ts | 2 +- .../configuration/flagEvaluationAggregator.ts | 7 +- .../src/configuration/flagEvaluationEvent.ts | 2 +- .../flagEvaluationEvent.types.ts | 2 +- packages/core/src/configuration/wire.ts | 2 +- .../core/src/evaluation/evaluateForSubject.ts | 5 +- packages/core/src/evaluation/evaluation.ts | 4 +- .../core/src/evaluation/evaluationMetadata.ts | 2 +- packages/core/src/time.ts | 2 + .../configuration/flagEvaluationEvent.spec.ts | 2 +- .../evaluation/evaluateForSubject.spec.ts | 2 +- packages/node-server/package.json | 3 +- packages/node-server/src/provider.ts | 3 +- scripts/assert-node-package-purity.js | 65 +++++++++++++++++++ scripts/test-node-package-install.sh | 2 + yarn.lock | 2 - 18 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/time.ts create mode 100644 scripts/assert-node-package-purity.js diff --git a/packages/core/package.json b/packages/core/package.json index f41f42b9..5f18ac9d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -52,7 +52,6 @@ "typescript": "^5.9.3" }, "dependencies": { - "@datadog/js-core": "0.0.3", "spark-md5": "^3.0.2" } } diff --git a/packages/core/src/configuration/configuration.ts b/packages/core/src/configuration/configuration.ts index 47fb6a81..0771f1e8 100644 --- a/packages/core/src/configuration/configuration.ts +++ b/packages/core/src/configuration/configuration.ts @@ -1,5 +1,5 @@ -import type { TimeStamp } from '@datadog/js-core/time' import type { EvaluationContext, FlagValueType, JsonValue, ResolutionReason } from '@openfeature/core' +import type { TimeStamp } from '../time' /** * Internal flags configuration for DatadogProvider. diff --git a/packages/core/src/configuration/exposureEvent.types.ts b/packages/core/src/configuration/exposureEvent.types.ts index 85afbc54..500c9843 100644 --- a/packages/core/src/configuration/exposureEvent.types.ts +++ b/packages/core/src/configuration/exposureEvent.types.ts @@ -1,5 +1,5 @@ -import type { TimeStamp } from '@datadog/js-core/time' import type { EvaluationContext } from '@openfeature/core' +import type { TimeStamp } from '../time' export interface ExposureEvent { allocation: { diff --git a/packages/core/src/configuration/flagEvaluationAggregator.ts b/packages/core/src/configuration/flagEvaluationAggregator.ts index 7a65ccd8..46b884a7 100644 --- a/packages/core/src/configuration/flagEvaluationAggregator.ts +++ b/packages/core/src/configuration/flagEvaluationAggregator.ts @@ -1,7 +1,6 @@ -import type { TimeStamp } from '@datadog/js-core/time' -import { timeStampNow } from '@datadog/js-core/time' import type { EvaluationContext, EvaluationContextValue, EvaluationDetails, FlagValue } from '@openfeature/core' import { getMD5Hash } from '../obfuscation' +import type { TimeStamp } from '../time' import { createFlagEvaluationEvent } from './flagEvaluationEvent' import type { FlagEvaluationEvent } from './flagEvaluationEvent.types' @@ -84,7 +83,7 @@ export class FlagEvaluationAggregator { return } - const flushTimestamp = timeStampNow() + const flushTimestamp = new Date().getTime() as TimeStamp const events = Array.from(this.aggregatedData.values()).map((data) => createFlagEvaluationEvent(data, flushTimestamp) ) @@ -118,7 +117,7 @@ export class FlagEvaluationAggregator { function getEvaluationTimestamp(details: EvaluationDetails): TimeStamp { const metadataTimestamp = details.flagMetadata?.[EVALUATION_TIMESTAMP_METADATA_KEY] - return Number.isFinite(metadataTimestamp) ? (metadataTimestamp as TimeStamp) : timeStampNow() + return Number.isFinite(metadataTimestamp) ? (metadataTimestamp as TimeStamp) : (new Date().getTime() as TimeStamp) } function isRuntimeDefaultUsed(details: EvaluationDetails): boolean { diff --git a/packages/core/src/configuration/flagEvaluationEvent.ts b/packages/core/src/configuration/flagEvaluationEvent.ts index 916ed8b3..1ef1dedd 100644 --- a/packages/core/src/configuration/flagEvaluationEvent.ts +++ b/packages/core/src/configuration/flagEvaluationEvent.ts @@ -1,5 +1,5 @@ -import type { TimeStamp } from '@datadog/js-core/time' import type { EvaluationContextValue } from '@openfeature/core' +import type { TimeStamp } from '../time' import type { FlagEvaluationEvent } from './flagEvaluationEvent.types' interface FlagEvaluationAggregationData { diff --git a/packages/core/src/configuration/flagEvaluationEvent.types.ts b/packages/core/src/configuration/flagEvaluationEvent.types.ts index 863fd457..cfbdfb23 100644 --- a/packages/core/src/configuration/flagEvaluationEvent.types.ts +++ b/packages/core/src/configuration/flagEvaluationEvent.types.ts @@ -1,5 +1,5 @@ -import type { TimeStamp } from '@datadog/js-core/time' import type { EvaluationContextValue } from '@openfeature/core' +import type { TimeStamp } from '../time' export interface FlagEvaluationEvent { flag: { diff --git a/packages/core/src/configuration/wire.ts b/packages/core/src/configuration/wire.ts index 615f8922..4bc2c1b3 100644 --- a/packages/core/src/configuration/wire.ts +++ b/packages/core/src/configuration/wire.ts @@ -1,5 +1,5 @@ -import type { TimeStamp } from '@datadog/js-core/time' import type { EvaluationContext } from '@openfeature/core' +import type { TimeStamp } from '../time' import type { FlagsConfiguration } from './configuration' type ConfigurationWire = { diff --git a/packages/core/src/evaluation/evaluateForSubject.ts b/packages/core/src/evaluation/evaluateForSubject.ts index 0fbd66b6..442e5853 100644 --- a/packages/core/src/evaluation/evaluateForSubject.ts +++ b/packages/core/src/evaluation/evaluateForSubject.ts @@ -1,7 +1,6 @@ -import type { TimeStamp } from '@datadog/js-core/time' -import { timeStampNow } from '@datadog/js-core/time' import type { ErrorCode, EvaluationContext, FlagValueType, Logger, ResolutionDetails } from '@openfeature/core' import type { FlagTypeToValue, PrecomputedFlagMetadata } from '../configuration' +import type { TimeStamp } from '../time' import { TargetingKeyMissingError } from './errors' import { createEvaluationTimestampMetadata } from './evaluationMetadata' import { matchesShard } from './matchesShard' @@ -15,7 +14,7 @@ export function evaluateForSubject( subjectAttributes: EvaluationContext, defaultValue: FlagTypeToValue, logger: Logger, - evaluationTimestampMs: TimeStamp = timeStampNow() + evaluationTimestampMs: TimeStamp = new Date().getTime() as TimeStamp ): ResolutionDetails> { if (!flag.enabled) { logger.debug(`returning default assignment because flag is disabled`, { diff --git a/packages/core/src/evaluation/evaluation.ts b/packages/core/src/evaluation/evaluation.ts index ff8e8be9..46428995 100644 --- a/packages/core/src/evaluation/evaluation.ts +++ b/packages/core/src/evaluation/evaluation.ts @@ -1,6 +1,6 @@ -import { timeStampNow } from '@datadog/js-core/time' import type { ErrorCode, EvaluationContext, FlagValueType, Logger, ResolutionDetails } from '@openfeature/core' import type { FlagTypeToValue } from '../configuration' +import type { TimeStamp } from '../time' import { TargetingKeyMissingError } from './errors' import { evaluateForSubject } from './evaluateForSubject' import { createEvaluationTimestampMetadata } from './evaluationMetadata' @@ -14,7 +14,7 @@ export function evaluateRulesBasedConfiguration( context: EvaluationContext, logger: Logger ): ResolutionDetails> { - const evaluationTimestampMs = timeStampNow() + const evaluationTimestampMs = new Date().getTime() as TimeStamp if (!config) { return { diff --git a/packages/core/src/evaluation/evaluationMetadata.ts b/packages/core/src/evaluation/evaluationMetadata.ts index b42a923e..d0be97d7 100644 --- a/packages/core/src/evaluation/evaluationMetadata.ts +++ b/packages/core/src/evaluation/evaluationMetadata.ts @@ -1,5 +1,5 @@ -import type { TimeStamp } from '@datadog/js-core/time' import type { PrecomputedFlagMetadata } from '../configuration' +import type { TimeStamp } from '../time' export function createEvaluationTimestampMetadata(evaluationTimestampMs: TimeStamp): PrecomputedFlagMetadata { return { __dd_eval_timestamp_ms: evaluationTimestampMs } as PrecomputedFlagMetadata diff --git a/packages/core/src/time.ts b/packages/core/src/time.ts new file mode 100644 index 00000000..d575a6ea --- /dev/null +++ b/packages/core/src/time.ts @@ -0,0 +1,2 @@ +/** Unix epoch timestamp in milliseconds. */ +export type TimeStamp = number & { t: 'Epoch time' } diff --git a/packages/core/test/configuration/flagEvaluationEvent.spec.ts b/packages/core/test/configuration/flagEvaluationEvent.spec.ts index 4cde63e7..08d42f38 100644 --- a/packages/core/test/configuration/flagEvaluationEvent.spec.ts +++ b/packages/core/test/configuration/flagEvaluationEvent.spec.ts @@ -1,5 +1,5 @@ -import type { TimeStamp } from '@datadog/js-core/time' import { createFlagEvaluationEvent } from '../../src/configuration/flagEvaluationEvent' +import type { TimeStamp } from '../../src/time' describe('createFlagEvaluationEvent', () => { it('should include targeting_key when it is a non-empty string', () => { diff --git a/packages/core/test/evaluation/evaluateForSubject.spec.ts b/packages/core/test/evaluation/evaluateForSubject.spec.ts index 919ed5a6..6ddc9d76 100644 --- a/packages/core/test/evaluation/evaluateForSubject.spec.ts +++ b/packages/core/test/evaluation/evaluateForSubject.spec.ts @@ -1,6 +1,6 @@ -import type { TimeStamp } from '@datadog/js-core/time' import type { EvaluationContext, Logger } from '@openfeature/core' import { evaluateForSubject, type Flag } from '../../src/evaluation' +import type { TimeStamp } from '../../src/time' describe('evaluateForSubject', () => { let logger: Logger diff --git a/packages/node-server/package.json b/packages/node-server/package.json index 8d9436fd..ffc0847c 100644 --- a/packages/node-server/package.json +++ b/packages/node-server/package.json @@ -36,8 +36,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@datadog/flagging-core": "2.0.1", - "@datadog/js-core": "0.0.3" + "@datadog/flagging-core": "2.0.1" }, "peerDependencies": { "@openfeature/server-sdk": ">=1.15.1" diff --git a/packages/node-server/src/provider.ts b/packages/node-server/src/provider.ts index 886d3f5d..333e8c67 100644 --- a/packages/node-server/src/provider.ts +++ b/packages/node-server/src/provider.ts @@ -5,7 +5,6 @@ import { type ExposureEvent, LRUInMemoryAssignmentCache, } from '@datadog/flagging-core' -import { timeStampNow } from '@datadog/js-core/time' import type { EvaluationContext } from '@openfeature/core' import type { EvaluationDetails, @@ -194,7 +193,7 @@ export class DatadogNodeServerProvider implements Provider { context: EvaluationContext, resolutionDetails: ResolutionDetails ): void { - const timestamp = timeStampNow() + const timestamp = new Date().getTime() const evalutationDetails: EvaluationDetails = { ...resolutionDetails, flagKey: flagKey, diff --git a/scripts/assert-node-package-purity.js b/scripts/assert-node-package-purity.js new file mode 100644 index 00000000..e1df8b60 --- /dev/null +++ b/scripts/assert-node-package-purity.js @@ -0,0 +1,65 @@ +'use strict' + +const fs = require('node:fs') +const path = require('node:path') + +const forbiddenPackageNames = [/^@datadog\/browser-/, /^@datadog\/js-core$/, /^@datadog\/openfeature-browser$/] +const browserSdkRepository = /github\.com[/:]DataDog\/browser-sdk(?:\.git)?$/i + +const found = new Set() +const visited = new Set() + +function inspectPackage(packageDirectory) { + let realDirectory + try { + realDirectory = fs.realpathSync(packageDirectory) + } catch { + return + } + + if (visited.has(realDirectory)) return + visited.add(realDirectory) + + const manifestPath = path.join(realDirectory, 'package.json') + if (fs.existsSync(manifestPath)) { + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) + const repository = typeof manifest.repository === 'string' ? manifest.repository : manifest.repository?.url + const isBrowserSdkDependency = + forbiddenPackageNames.some((pattern) => pattern.test(manifest.name)) || + browserSdkRepository.test(repository ?? '') + if (isBrowserSdkDependency) { + found.add(manifest.name) + } + } + + inspectNodeModules(path.join(realDirectory, 'node_modules')) +} + +function inspectNodeModules(nodeModulesDirectory) { + if (!fs.existsSync(nodeModulesDirectory)) return + + for (const entry of fs.readdirSync(nodeModulesDirectory, { withFileTypes: true })) { + if (entry.name.startsWith('.')) continue + + const entryPath = path.join(nodeModulesDirectory, entry.name) + if (entry.name.startsWith('@')) { + for (const scopedEntry of fs.readdirSync(entryPath, { withFileTypes: true })) { + if (scopedEntry.isDirectory() || scopedEntry.isSymbolicLink()) { + inspectPackage(path.join(entryPath, scopedEntry.name)) + } + } + } else if (entry.isDirectory() || entry.isSymbolicLink()) { + inspectPackage(entryPath) + } + } +} + +inspectNodeModules(path.resolve(process.argv[2] ?? 'node_modules')) + +if (found.size > 0) { + console.error('ERROR: Node packages must not install browser SDK dependencies') + for (const packageName of [...found].sort()) console.error(packageName) + process.exit(1) +} + +console.log('Verified Node packages do not install browser SDK dependencies') diff --git a/scripts/test-node-package-install.sh b/scripts/test-node-package-install.sh index a2a97908..2e2e700f 100755 --- a/scripts/test-node-package-install.sh +++ b/scripts/test-node-package-install.sh @@ -125,6 +125,8 @@ if [ "$WITH_OPENFEATURE" = true ]; then fi fi +node "$REPO_ROOT/scripts/assert-node-package-purity.js" node_modules + # Run tests echo "" echo "Running tests..." diff --git a/yarn.lock b/yarn.lock index 6c4a57ed..ec89a87f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -663,7 +663,6 @@ __metadata: version: 0.0.0-use.local resolution: "@datadog/flagging-core@workspace:packages/core" dependencies: - "@datadog/js-core": "npm:0.0.3" "@openfeature/core": "npm:^1.9.2" "@types/jest": "npm:^30.0.0" "@types/node": "npm:^18.0.0" @@ -741,7 +740,6 @@ __metadata: resolution: "@datadog/openfeature-node-server@workspace:packages/node-server" dependencies: "@datadog/flagging-core": "npm:2.0.1" - "@datadog/js-core": "npm:0.0.3" "@openfeature/core": "npm:1.9.2" "@openfeature/server-sdk": "npm:1.20.2" "@types/jest": "npm:^30.0.0" From 459352867b471ae9c332fd3ee697321fa2dcdbd5 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 21 Jul 2026 23:03:20 -0600 Subject: [PATCH 2/2] refactor(core): share timestamp helper --- packages/core/src/configuration/flagEvaluationAggregator.ts | 6 +++--- packages/core/src/evaluation/evaluateForSubject.ts | 4 ++-- packages/core/src/evaluation/evaluation.ts | 4 ++-- packages/core/src/index.ts | 1 + packages/core/src/time.ts | 4 ++++ packages/node-server/src/provider.ts | 3 ++- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/core/src/configuration/flagEvaluationAggregator.ts b/packages/core/src/configuration/flagEvaluationAggregator.ts index 46b884a7..fba28568 100644 --- a/packages/core/src/configuration/flagEvaluationAggregator.ts +++ b/packages/core/src/configuration/flagEvaluationAggregator.ts @@ -1,6 +1,6 @@ import type { EvaluationContext, EvaluationContextValue, EvaluationDetails, FlagValue } from '@openfeature/core' import { getMD5Hash } from '../obfuscation' -import type { TimeStamp } from '../time' +import { type TimeStamp, timeStampNow } from '../time' import { createFlagEvaluationEvent } from './flagEvaluationEvent' import type { FlagEvaluationEvent } from './flagEvaluationEvent.types' @@ -83,7 +83,7 @@ export class FlagEvaluationAggregator { return } - const flushTimestamp = new Date().getTime() as TimeStamp + const flushTimestamp = timeStampNow() const events = Array.from(this.aggregatedData.values()).map((data) => createFlagEvaluationEvent(data, flushTimestamp) ) @@ -117,7 +117,7 @@ export class FlagEvaluationAggregator { function getEvaluationTimestamp(details: EvaluationDetails): TimeStamp { const metadataTimestamp = details.flagMetadata?.[EVALUATION_TIMESTAMP_METADATA_KEY] - return Number.isFinite(metadataTimestamp) ? (metadataTimestamp as TimeStamp) : (new Date().getTime() as TimeStamp) + return Number.isFinite(metadataTimestamp) ? (metadataTimestamp as TimeStamp) : timeStampNow() } function isRuntimeDefaultUsed(details: EvaluationDetails): boolean { diff --git a/packages/core/src/evaluation/evaluateForSubject.ts b/packages/core/src/evaluation/evaluateForSubject.ts index 442e5853..6e4dc200 100644 --- a/packages/core/src/evaluation/evaluateForSubject.ts +++ b/packages/core/src/evaluation/evaluateForSubject.ts @@ -1,6 +1,6 @@ import type { ErrorCode, EvaluationContext, FlagValueType, Logger, ResolutionDetails } from '@openfeature/core' import type { FlagTypeToValue, PrecomputedFlagMetadata } from '../configuration' -import type { TimeStamp } from '../time' +import { type TimeStamp, timeStampNow } from '../time' import { TargetingKeyMissingError } from './errors' import { createEvaluationTimestampMetadata } from './evaluationMetadata' import { matchesShard } from './matchesShard' @@ -14,7 +14,7 @@ export function evaluateForSubject( subjectAttributes: EvaluationContext, defaultValue: FlagTypeToValue, logger: Logger, - evaluationTimestampMs: TimeStamp = new Date().getTime() as TimeStamp + evaluationTimestampMs: TimeStamp = timeStampNow() ): ResolutionDetails> { if (!flag.enabled) { logger.debug(`returning default assignment because flag is disabled`, { diff --git a/packages/core/src/evaluation/evaluation.ts b/packages/core/src/evaluation/evaluation.ts index 46428995..5ddd83ef 100644 --- a/packages/core/src/evaluation/evaluation.ts +++ b/packages/core/src/evaluation/evaluation.ts @@ -1,6 +1,6 @@ import type { ErrorCode, EvaluationContext, FlagValueType, Logger, ResolutionDetails } from '@openfeature/core' import type { FlagTypeToValue } from '../configuration' -import type { TimeStamp } from '../time' +import { timeStampNow } from '../time' import { TargetingKeyMissingError } from './errors' import { evaluateForSubject } from './evaluateForSubject' import { createEvaluationTimestampMetadata } from './evaluationMetadata' @@ -14,7 +14,7 @@ export function evaluateRulesBasedConfiguration( context: EvaluationContext, logger: Logger ): ResolutionDetails> { - const evaluationTimestampMs = new Date().getTime() as TimeStamp + const evaluationTimestampMs = timeStampNow() if (!config) { return { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 56deefff..018d0574 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,6 +2,7 @@ export * from './cache' export * from './configuration' export * from './evaluation' export * from './obfuscation' +export * from './time' // Build environment placeholder for testing const _SDK_VERSION = __BUILD_ENV__SDK_VERSION__ diff --git a/packages/core/src/time.ts b/packages/core/src/time.ts index d575a6ea..d923b141 100644 --- a/packages/core/src/time.ts +++ b/packages/core/src/time.ts @@ -1,2 +1,6 @@ /** Unix epoch timestamp in milliseconds. */ export type TimeStamp = number & { t: 'Epoch time' } + +export function timeStampNow(): TimeStamp { + return new Date().getTime() as TimeStamp +} diff --git a/packages/node-server/src/provider.ts b/packages/node-server/src/provider.ts index 333e8c67..380fee59 100644 --- a/packages/node-server/src/provider.ts +++ b/packages/node-server/src/provider.ts @@ -4,6 +4,7 @@ import { createExposureEvent, type ExposureEvent, LRUInMemoryAssignmentCache, + timeStampNow, } from '@datadog/flagging-core' import type { EvaluationContext } from '@openfeature/core' import type { @@ -193,7 +194,7 @@ export class DatadogNodeServerProvider implements Provider { context: EvaluationContext, resolutionDetails: ResolutionDetails ): void { - const timestamp = new Date().getTime() + const timestamp = timeStampNow() const evalutationDetails: EvaluationDetails = { ...resolutionDetails, flagKey: flagKey,