Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Browser integration bundles now share context providers and the configured log level with TSJS core (#1196). With Permutive enabled and cohorts available, `tsjs.requestAds()` now sends `config.permutive_segments` to `/auction`; the server retains that key only when `auction.allowed_context_keys` includes `"permutive_segments"`. This restores a previously inactive data flow, not a consent gate: review publisher consent handling before relying on it. Integration logs now follow `tsjs.setConfig` and `tsjs.log.setLevel`, and creative no longer raises the default `warn` level to `info`. The `?tsdebug=1` query parameter or `localStorage['tsdebug'] = '1'` intentionally enables `debug` logging across all bundles when creative installs, overriding any log level set earlier through `tsjs.setConfig` or `tsjs.log.setLevel`. Later calls to either API can change the level again.
- `[auction].allowed_context_keys` now serializes in sorted, deduplicated order, so ESI template-cache fingerprints and `ts config diff`/`push` envelope hashes are stable across loads. Template fingerprints also sort object keys independently of `serde_json/preserve_order`. Existing envelopes may show a one-time allowlist reorder after upgrading; push once to settle it. The updated fingerprint format causes one template-cache miss per cached page after deployment.
- TSJS-generated envelopes now send `trustedServer.params.storedRequest: false`, preventing accidental PBS stored lookups without suppressing eligible non-PBS demand. PBS filters unusable impressions after overrides; explicit `true` and omission in valid envelopes retain inline-first stored fallback. A malformed envelope disables stored fallback for the entire slot, including independent direct demand left unusable after overrides. Publisher intent survives repeated and refresh auctions. Deploy compatible server admission everywhere before serving the new JS, and retain it during rollback while cached clients remain. See the Prebid deployment guide.
- Protocol-relative creative URLs now honor `rewrite.exclude_domains`, so excluded creative assets stay direct and excluded absolute or protocol-relative URLs submitted to `/first-party/sign` are rejected.
Expand Down
29 changes: 4 additions & 25 deletions crates/trusted-server-js/lib/build-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { build } from 'vite';

import { moduleBuildOptions } from './build-module-options.mjs';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const srcDir = path.resolve(__dirname, 'src');
const distDir = path.resolve(__dirname, '..', 'dist');
Expand All @@ -39,7 +41,7 @@ const integrationModules = fs.existsSync(integrationsDir)
);
})
.sort()
: [];
: [];

console.log('[build-all] Discovered integrations:', integrationModules);

Expand All @@ -48,30 +50,7 @@ async function buildModule(name, entryPath) {
const outFile = `tsjs-${name}.js`;
console.log(`[build-all] Building ${outFile} from ${path.relative(__dirname, entryPath)}`);

await build({
configFile: false,
root: __dirname,
build: {
emptyOutDir: false,
outDir: distDir,
assetsDir: '.',
sourcemap: false,
minify: 'esbuild',
rollupOptions: {
input: entryPath,
output: {
format: 'iife',
dir: distDir,
entryFileNames: outFile,
inlineDynamicImports: true,
extend: false,
// Use a unique IIFE name per module to avoid conflicts
name: name === 'core' ? 'tsjs' : `tsjs_${name}`,
},
},
},
logLevel: 'warn',
});
await build(moduleBuildOptions({ name, entryPath, outDir: distDir }));

console.log(`[build-all] Built ${outFile}`);
}
Expand Down
32 changes: 32 additions & 0 deletions crates/trusted-server-js/lib/build-module-options.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const libDir = path.dirname(fileURLToPath(import.meta.url));

/** Production Vite options for one independently bundled tsjs IIFE. */
export function moduleBuildOptions({ name, entryPath, outDir }) {
return {
configFile: false,
root: libDir,
build: {
emptyOutDir: false,
outDir,
assetsDir: '.',
sourcemap: false,
minify: 'esbuild',
rollupOptions: {
input: entryPath,
output: {
format: 'iife',
dir: outDir,
entryFileNames: `tsjs-${name}.js`,
inlineDynamicImports: true,
extend: false,
// Use a unique IIFE name per module to avoid conflicts.
name: name === 'core' ? 'tsjs' : `tsjs_${name}`,
},
},
},
logLevel: 'warn',
};
}
14 changes: 12 additions & 2 deletions crates/trusted-server-js/lib/src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ import { log } from './log';
*/
export type ContextProvider = () => Record<string, unknown> | undefined;

const providers = new Map<string, ContextProvider>();
// Independently built IIFEs must register and collect from the same map.
// Resolve it on each call rather than capturing a bundle-local registry.
const CONTEXT_PROVIDERS_KEY = Symbol.for('trusted-server.contextProviders');

function getProviders(): Map<string, ContextProvider> {
const sharedGlobal = globalThis as typeof globalThis & {
[CONTEXT_PROVIDERS_KEY]?: Map<string, ContextProvider>;
};
return (sharedGlobal[CONTEXT_PROVIDERS_KEY] ??= new Map<string, ContextProvider>());
}
Comment thread
ChristianPavilonis marked this conversation as resolved.

/**
* Register a context provider that will be called before every auction request.
Expand All @@ -19,6 +28,7 @@ const providers = new Map<string, ContextProvider>();
* duplicate accumulation in SPA environments.
*/
export function registerContextProvider(id: string, provider: ContextProvider): void {
const providers = getProviders();
providers.set(id, provider);
log.debug('context: registered provider', { id, total: providers.size });
}
Expand All @@ -32,7 +42,7 @@ export function registerContextProvider(id: string, provider: ContextProvider):
*/
export function collectContext(): Record<string, unknown> {
const context: Record<string, unknown> = {};
for (const provider of providers.values()) {
for (const provider of getProviders().values()) {
try {
const data = provider();
if (data) Object.assign(context, data);
Expand Down
23 changes: 14 additions & 9 deletions crates/trusted-server-js/lib/src/core/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';

const LEVELS: Record<LogLevel, number> = { silent: -1, error: 0, warn: 1, info: 2, debug: 3 };
let currentLevel: LogLevel = 'warn';
// Each IIFE carries its own logger code, but all copies share one level.
const LOG_LEVEL_KEY = Symbol.for('trusted-server.logLevel');

type LogGlobal = typeof globalThis & { [LOG_LEVEL_KEY]?: LogLevel };

function getLevel(): LogLevel {
return ((globalThis as LogGlobal)[LOG_LEVEL_KEY] ??= 'warn');
}

function levelNum(l: LogLevel) {
return LEVELS[l] ?? 1;
Expand Down Expand Up @@ -50,21 +57,19 @@ function print(method: 'log' | 'info' | 'warn' | 'error', ...args: unknown[]) {
// Thin wrapper around console that keeps timestamped output and honours a runtime log level.
export const log = {
setLevel(l: LogLevel) {
currentLevel = l;
},
getLevel(): LogLevel {
return currentLevel;
(globalThis as LogGlobal)[LOG_LEVEL_KEY] = l;
},
getLevel,
info: (...a: unknown[]) => {
if (levelNum(currentLevel) >= LEVELS.info) print('info', ...a);
if (levelNum(getLevel()) >= LEVELS.info) print('info', ...a);
},
warn: (...a: unknown[]) => {
if (levelNum(currentLevel) >= LEVELS.warn) print('warn', ...a);
if (levelNum(getLevel()) >= LEVELS.warn) print('warn', ...a);
},
error: (...a: unknown[]) => {
if (levelNum(currentLevel) >= LEVELS.error) print('error', ...a);
if (levelNum(getLevel()) >= LEVELS.error) print('error', ...a);
},
debug: (...a: unknown[]) => {
if (levelNum(currentLevel) >= LEVELS.debug) print('log', ...a);
if (levelNum(getLevel()) >= LEVELS.debug) print('log', ...a);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,6 @@ function monitorAnchorMutations(): void {

// Wire up capture-phase click handlers + mutation observers to protect clicks.
export function installClickGuard(): void {
if (log.getLevel && log.getLevel() === 'warn') {
log.setLevel('info');
}
enableDebugFromEnv();
Comment thread
ChristianPavilonis marked this conversation as resolved.
log.info('tsjs-creative:click: installing click guard');

Expand Down
15 changes: 11 additions & 4 deletions crates/trusted-server-js/lib/test/core/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

const LOG_LEVEL_KEY = Symbol.for('trusted-server.logLevel');
const sharedGlobal = globalThis as typeof globalThis & { [LOG_LEVEL_KEY]?: unknown };

describe('config', () => {
beforeEach(async () => {
// reset module state between tests
await vi.resetModules();
beforeEach(() => {
delete sharedGlobal[LOG_LEVEL_KEY];
vi.resetModules();
});

afterEach(() => {
delete sharedGlobal[LOG_LEVEL_KEY];
});

it('sets and gets config, controls log level', async () => {
Expand Down
27 changes: 24 additions & 3 deletions crates/trusted-server-js/lib/test/core/context.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

const CONTEXT_PROVIDERS_KEY = Symbol.for('trusted-server.contextProviders');
const sharedGlobal = globalThis as typeof globalThis & {
[CONTEXT_PROVIDERS_KEY]?: unknown;
};

describe('context provider registry', () => {
beforeEach(async () => {
await vi.resetModules();
beforeEach(() => {
delete sharedGlobal[CONTEXT_PROVIDERS_KEY];
vi.resetModules();
});

afterEach(() => {
delete sharedGlobal[CONTEXT_PROVIDERS_KEY];
});

it('returns empty context when no providers registered', async () => {
Expand Down Expand Up @@ -46,6 +56,17 @@ describe('context provider registry', () => {
expect(collectContext()).toEqual({ survived: true });
});

it('shares providers between separately loaded module instances', async () => {
const first = await import('../../src/core/context');
first.registerContextProvider('first', () => ({ a: 1 }));

vi.resetModules();
const second = await import('../../src/core/context');
expect(second.collectContext()).toEqual({ a: 1 });
second.registerContextProvider('second', () => ({ b: 2 }));
expect(first.collectContext()).toEqual({ a: 1, b: 2 });
});

it('re-registration with same id replaces previous provider', async () => {
const { registerContextProvider, collectContext } = await import('../../src/core/context');
registerContextProvider('dup', () => ({ v: 1 }));
Expand Down
57 changes: 57 additions & 0 deletions crates/trusted-server-js/lib/test/core/log.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const LOG_LEVEL_KEY = Symbol.for('trusted-server.logLevel');
const sharedGlobal = globalThis as typeof globalThis & {
[LOG_LEVEL_KEY]?: unknown;
};

describe('shared logger', () => {
beforeEach(() => {
delete sharedGlobal[LOG_LEVEL_KEY];
vi.resetModules();
});

afterEach(() => {
delete sharedGlobal[LOG_LEVEL_KEY];
vi.restoreAllMocks();
});

it('defaults to warn', async () => {
const { log } = await import('../../src/core/log');
expect(log.getLevel()).toBe('warn');
});

it('shares level changes between separately loaded module instances', async () => {
const first = await import('../../src/core/log');
first.log.setLevel('debug');

vi.resetModules();
const second = await import('../../src/core/log');
expect(second.log.getLevel()).toBe('debug');
second.log.setLevel('silent');
expect(first.log.getLevel()).toBe('silent');
});

it.each([
['silent', []],
['error', ['error']],
['warn', ['error', 'warn']],
['info', ['error', 'warn', 'info']],
['debug', ['error', 'warn', 'info', 'debug']],
] as const)('gates all console methods at %s', async (level, enabled) => {
const spies = {
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
info: vi.spyOn(console, 'info').mockImplementation(() => {}),
debug: vi.spyOn(console, 'log').mockImplementation(() => {}),
};
const { log } = await import('../../src/core/log');
log.setLevel(level);
for (const method of ['error', 'warn', 'info', 'debug'] as const) {
log[method]('example');
expect(spies[method]).toHaveBeenCalledTimes(
enabled.some((value) => value === method) ? 1 : 0
);
}
});
});
Loading
Loading