Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/docs/src/content/inspectors/ssr-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ The devtools don't write SSR mocks to TransferState, so the browser requests the

Client rules apply right away. SSR rules apply from the next page load, so the panel asks for a reload only when a rule applies on SSR. The page also keeps client rules in `sessionStorage`, so they apply on reload before the overlay connects. Rules live in the memory of the server process. They survive a Vite restart in the same process, such as after a config edit, and the **SSR & HTTP** tab keeps showing them. A new process starts with none.

If the `http` inspector or `actions.http` is off, the server clears its rules when it starts. SSR rules apply only while the devtools server runs, so they stop when it closes, for example after a config edit that removes the Vite plugin. The overlay removes the stored client rules when it connects with the `http` inspector off, so requests made before it connects on that load can still fail.
If the `http` inspector or `actions.http` is off, the server clears its rules when it starts. SSR rules apply only while the devtools server runs, so they stop when it closes, for example after a config edit that removes the Vite plugin. When the overlay connects with the `http` inspector or `actions.http` off, it removes the stored client rules and the page stops applying client rules until a later load turns both back on. Requests made before the overlay connects on that load can still fail once.

### Timeline and rule caps

Expand Down
88 changes: 85 additions & 3 deletions packages/devtools/src/__tests__/overlay-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
// @vitest-environment jsdom
import '@angular/compiler';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { HttpRequest, HttpResponse } from '@angular/common/http';
import { Injector, PLATFORM_ID, runInInjectionContext } from '@angular/core';
import { of } from 'rxjs';
import type { PangularConfig } from '../config.ts';
import { isSecretKey, setRedaction } from '../forms-privacy.ts';
import { RULES_STORAGE_KEY, clientRules, httpRegistry, storeRules } from '../http-rules.ts';
import { pangularHttpInterceptor } from '../http.ts';
import {
RULES_STORAGE_KEY,
clientRules,
httpRegistry,
storeRules,
type HttpRule,
} from '../http-rules.ts';
import { noteFailedCall, setNavigationLimit, type NavigationRecord } from '../router.ts';

const calls: string[] = [];
const sentArgs = new Map<string, unknown>();
const replies = new Map<string, unknown>();
let configs: Record<string, unknown> | undefined;

vi.mock('devframe/client', () => ({
Expand All @@ -17,7 +29,7 @@ vi.mock('devframe/client', () => ({
call: async (name: string, arg?: unknown) => {
calls.push(name);
sentArgs.set(name, arg);
return undefined;
return replies.get(name);
},
register: () => {},
},
Expand All @@ -39,13 +51,47 @@ async function start(config?: PangularConfig) {
afterEach(() => {
stops.splice(0).forEach((stop) => stop());
sessionStorage.clear();
replies.clear();
setRedaction();
setNavigationLimit(50);
delete httpRegistry().maxCalls;
delete httpRegistry().rules;
delete httpRegistry().rulesOff;
vi.restoreAllMocks();
});

const faultRule: HttpRule = {
id: 'r1',
pattern: '/api',
enabled: true,
target: 'client',
status: 500,
};

function reload() {
stops.splice(0).forEach((stop) => stop());
delete httpRegistry().rules;
delete httpRegistry().rulesOff;
}

function requestFaulted(): boolean {
const g = globalThis as { ngDevMode?: unknown };
const saved = g.ngDevMode;
g.ngDevMode ??= true;
httpRegistry().calls = [];
const injector = Injector.create({ providers: [{ provide: PLATFORM_ID, useValue: 'browser' }] });
try {
runInInjectionContext(injector, () =>
pangularHttpInterceptor(new HttpRequest('GET', '/api/products'), () =>
of(new HttpResponse({ status: 200, body: [] })),
),
).subscribe({ error: () => {} });
return httpRegistry().calls?.at(-1)?.faulted === true;
} finally {
g.ngDevMode = saved;
}
}

describe('overlay collectors', () => {
it('ping instead of resending unchanged trees and HTTP calls', async () => {
await start({ limits: { refreshMs: 500 } });
Expand Down Expand Up @@ -147,11 +193,47 @@ describe('overlay collectors', () => {
});

it('drop stored fault rules when the http inspector is off', async () => {
storeRules([{ id: 'r1', pattern: '/api', enabled: true, target: 'client', status: 500 }]);
storeRules([faultRule]);
expect(requestFaulted()).toBe(true);
await start({ inspectors: { http: false } });
expect(sessionStorage.getItem(RULES_STORAGE_KEY)).toBeNull();
expect(requestFaulted()).toBe(false);
reload();
expect(clientRules()).toEqual([]);
expect(requestFaulted()).toBe(false);
});

it('drop stored fault rules when the http action is off', async () => {
replies.set('get-http-rules', [faultRule]);
storeRules([faultRule]);
await start({ actions: { http: false } });
expect(sessionStorage.getItem(RULES_STORAGE_KEY)).toBeNull();
expect(requestFaulted()).toBe(false);
reload();
expect(clientRules()).toEqual([]);
expect(requestFaulted()).toBe(false);
});

it('ignore rules stored after the config turned http off', async () => {
await start({ inspectors: { http: false } });
storeRules([faultRule]);
sessionStorage.setItem(RULES_STORAGE_KEY, JSON.stringify([faultRule]));
delete httpRegistry().rules;
expect(clientRules()).toEqual([]);
expect(requestFaulted()).toBe(false);
});

it('keep fault rules across a reload while http is on', async () => {
replies.set('get-http-rules', [faultRule]);
storeRules([faultRule]);
await start();
expect(requestFaulted()).toBe(true);
reload();
expect(clientRules()).toEqual([faultRule]);
expect(requestFaulted()).toBe(true);
await start();
expect(JSON.parse(sessionStorage.getItem(RULES_STORAGE_KEY) ?? '[]')).toEqual([faultRule]);
expect(requestFaulted()).toBe(true);
});

it('apply the redaction config from the server before collecting', async () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/devtools/src/http-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface HttpRegistry {
dispose?: () => void;
/** The hub context whose setup installed `record` and `dispose`. */
owner?: unknown;
rulesOff?: boolean;
}

export const MAX_CALLS = 200;
Expand All @@ -71,7 +72,9 @@ export const RULES_STORAGE_KEY = 'pangular:http-rules';
* next load, before the overlay connects.
*/
export function storeRules(rules: HttpRule[]) {
httpRegistry().rules = rules;
const registry = httpRegistry();
if (registry.rulesOff) rules = [];
registry.rules = rules;
try {
if (rules.length) sessionStorage.setItem(RULES_STORAGE_KEY, JSON.stringify(rules));
else sessionStorage.removeItem(RULES_STORAGE_KEY);
Expand All @@ -80,8 +83,14 @@ export function storeRules(rules: HttpRule[]) {
}
}

export function allowClientRules(allowed: boolean) {
httpRegistry().rulesOff = !allowed;
if (!allowed) storeRules([]);
}

export function clientRules(): HttpRule[] {
const registry = httpRegistry();
if (registry.rulesOff) return [];
if (!registry.rules) {
try {
registry.rules = sanitizeRules(JSON.parse(sessionStorage.getItem(RULES_STORAGE_KEY) ?? '[]'));
Expand Down
4 changes: 2 additions & 2 deletions packages/devtools/src/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { attachAnalog } from './analog-runtime.ts';
import { attachForms } from './forms-collector.ts';
import { attachPipes } from './pipes-collector.ts';
import { attachHttp } from './http-overlay.ts';
import { httpRegistry, storeRules } from './http-rules.ts';
import { allowClientRules, httpRegistry } from './http-rules.ts';
import { attachNgrx } from './ngrx-overlay.ts';
import { collectInjectorTree } from './injector-tree.ts';
import {
Expand Down Expand Up @@ -197,7 +197,7 @@ async function startOverlay(options: OverlayOptions, own: (cleanup: () => void)
const devtoolsConfig = configFromConnection(rpc.connectionMeta);
const on = devtoolsConfig.inspectors;
const limits = devtoolsConfig.limits;
if (!on.http) storeRules([]);
allowClientRules(on.http && devtoolsConfig.actions.http);
setRedaction(devtoolsConfig.redaction);
setNavigationLimit(limits.navigations);
if (on.http) httpRegistry().maxCalls = limits.httpCalls;
Expand Down
Loading