From 04d00f4459748246ed79922d9623d5238be28c45 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 20 Jul 2026 23:07:57 +0000 Subject: [PATCH 1/2] feat(core): expose debuggableFn for effects in signal debug graph Exposes the internal fn reference of effect reactive nodes on DebugSignalGraphNode as debuggableFn. This allows Angular DevTools to inspect and set breakpoints on effect callbacks. --- packages/core/src/render3/util/signal_debug.ts | 8 ++++++++ packages/core/test/acceptance/signal_debug_spec.ts | 2 ++ 2 files changed, 10 insertions(+) diff --git a/packages/core/src/render3/util/signal_debug.ts b/packages/core/src/render3/util/signal_debug.ts index ffab39a19402..cf08c44d27be 100644 --- a/packages/core/src/render3/util/signal_debug.ts +++ b/packages/core/src/render3/util/signal_debug.ts @@ -104,6 +104,14 @@ function getNodesAndEdgesFromSignalMap(signalMap: ReadonlyMap unknown) | undefined, id, }); + } else if (isEffectNode(consumer)) { + debugSignalGraphNodes.push({ + label: consumer.debugName, + kind: consumer.kind, + epoch: consumer.version, + debuggableFn: consumer.fn, + id, + }); } else { debugSignalGraphNodes.push({ label: consumer.debugName, diff --git a/packages/core/test/acceptance/signal_debug_spec.ts b/packages/core/test/acceptance/signal_debug_spec.ts index b310cf0365fe..b4ae8b773bb1 100644 --- a/packages/core/test/acceptance/signal_debug_spec.ts +++ b/packages/core/test/acceptance/signal_debug_spec.ts @@ -119,6 +119,8 @@ describe('getSignalGraph', () => { const effectNode = nodes.find((node) => node.label === 'primitiveSignalEffect')!; expect(effectNode).toBeDefined(); + expect(effectNode.debuggableFn).toBeDefined(); + expect(typeof effectNode.debuggableFn).toBe('function'); const signalNode = nodes.find((node) => node.label === 'primitiveSignal')!; expect(signalNode).toBeDefined(); From 41b55c20b489bb16fd105e44d566f7143ad7d4b6 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 20 Jul 2026 23:07:58 +0000 Subject: [PATCH 2/2] feat(devtools): implement persistent breakpoints on signal consumers using CDP Implements persistent breakpoints for Angular signal consumers (computeds and effects) in Angular DevTools using Chrome DevTools Protocol (CDP). - Adds debugger permission to Chrome and Firefox extension manifests. - Implements CDP breakpoint orchestration in background script. - Updates SignalDetailsComponent and SignalGraphPaneComponent with label icon toggle and persistent state sync across reloads. --- .../src/lib/application-operations/index.ts | 3 + .../test-utils/app_operations_mock.ts | 12 ++ .../signal-graph-pane.component.html | 3 + .../signal-graph-pane.component.ts | 64 +++++++ .../signal-details.component.html | 11 ++ .../signal-details.component.ts | 3 + .../shell-browser/src/app/background.ts | 179 ++++++++++++++++++ .../src/app/chrome-application-operations.ts | 51 +++++ .../src/manifest/manifest.chrome.json | 2 +- .../src/manifest/manifest.firefox.json | 2 +- devtools/src/demo-application-operations.ts | 16 ++ 11 files changed, 344 insertions(+), 2 deletions(-) diff --git a/devtools/projects/ng-devtools/src/lib/application-operations/index.ts b/devtools/projects/ng-devtools/src/lib/application-operations/index.ts index ae00f791819f..b8820e840e96 100644 --- a/devtools/projects/ng-devtools/src/lib/application-operations/index.ts +++ b/devtools/projects/ng-devtools/src/lib/application-operations/index.ts @@ -14,6 +14,9 @@ export abstract class ApplicationOperations { abstract selectDomElement(position: ElementPosition, target: Frame): void; abstract inspect(directivePosition: DirectivePosition, objectPath: string[], target: Frame): void; abstract inspectSignal(position: SignalNodePosition, target: Frame): void; + abstract setSignalBreakpoint(position: SignalNodePosition, target: Frame): void; + abstract removeSignalBreakpoint(position: SignalNodePosition, target: Frame): void; + abstract getActiveSignalBreakpoints(target: Frame): Promise; abstract viewSourceFromRouter(name: string, type: string, target: Frame): void; abstract setStorageItems(items: {[key: string]: unknown}): Promise; abstract getStorageItems(items: string[]): Promise<{[key: string]: unknown}>; diff --git a/devtools/projects/ng-devtools/src/lib/application-services/test-utils/app_operations_mock.ts b/devtools/projects/ng-devtools/src/lib/application-services/test-utils/app_operations_mock.ts index 7e3aff5a4550..937231a55472 100644 --- a/devtools/projects/ng-devtools/src/lib/application-services/test-utils/app_operations_mock.ts +++ b/devtools/projects/ng-devtools/src/lib/application-services/test-utils/app_operations_mock.ts @@ -58,6 +58,18 @@ export class AppOperationsMock extends ApplicationOperations { throw new Error('Method not implemented.'); } + override setSignalBreakpoint(position: SignalNodePosition, target: Frame): void { + throw new Error('Method not implemented.'); + } + + override removeSignalBreakpoint(position: SignalNodePosition, target: Frame): void { + throw new Error('Method not implemented.'); + } + + override getActiveSignalBreakpoints(target: Frame): Promise { + return Promise.resolve([]); + } + override viewSourceFromRouter(name: string, type: string, target: Frame): void { throw new Error('Method not implemented.'); } diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/signal-graph-pane/signal-graph-pane.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/signal-graph-pane/signal-graph-pane.component.html index 445b9ed22e90..2f3affe1bbf0 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/signal-graph-pane/signal-graph-pane.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/signal-graph-pane/signal-graph-pane.component.html @@ -15,7 +15,10 @@ [node]="node" [graph]="signalGraph.graph()!" [element]="signalGraph.element()!" + [hasBreakpoint]="hasBreakpoint(node)" (gotoSource)="gotoSource($event)" + (setBreakpoint)="setBreakpoint($event)" + (removeBreakpoint)="removeBreakpoint($event)" (expandCluster)="expandCluster($event)" (highlightDeps)="highlightDeps($event)" (close)="detailsVisible.set(false)" diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/signal-graph-pane/signal-graph-pane.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/signal-graph-pane/signal-graph-pane.component.ts index 87673a31e5cf..944c7f99682d 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/signal-graph-pane/signal-graph-pane.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/signal-graph-pane/signal-graph-pane.component.ts @@ -9,6 +9,7 @@ import { Component, computed, + effect, inject, input, linkedSignal, @@ -45,6 +46,25 @@ export class SignalGraphPaneComponent { private readonly appOperations = inject(ApplicationOperations); private readonly frameManager = inject(FrameManager); + constructor() { + effect(() => { + const element = this.signalGraph.element(); + const graph = this.signalGraph.graph(); + if (element && graph) { + const frame = this.frameManager.selectedFrame(); + this.appOperations.getActiveSignalBreakpoints(frame!).then((positions) => { + const activeIds = new Set(); + for (const pos of positions) { + if (JSON.stringify(pos.element) === JSON.stringify(element)) { + activeIds.add(pos.signalId); + } + } + this.activeBreakpoints.set(activeIds); + }); + } + }); + } + protected readonly close = output(); // Source for selected node ID. @@ -86,6 +106,18 @@ export class SignalGraphPaneComponent { protected readonly detailsVisible = signal(false); + // Track active breakpoints for the current inspected element. + // Resets when the inspected element changes. + protected readonly activeBreakpoints = linkedSignal>({ + source: () => this.signalGraph.element(), + computation: () => new Set(), + }); + + protected hasBreakpoint(node: DevtoolsSignalGraphNode | undefined): boolean { + if (!node) return false; + return this.activeBreakpoints().has(node.id); + } + protected empty = computed(() => !(this.signalGraph.graph()?.nodes.length! > 0)); onNodeClick(node: DevtoolsSignalGraphNode) { @@ -104,6 +136,38 @@ export class SignalGraphPaneComponent { ); } + setBreakpoint(node: DevtoolsSignalGraphNode) { + const frame = this.frameManager.selectedFrame(); + this.appOperations.setSignalBreakpoint( + { + element: this.signalGraph.element()!, + signalId: node.id, + }, + frame!, + ); + this.activeBreakpoints.update((set) => { + const newSet = new Set(set); + newSet.add(node.id); + return newSet; + }); + } + + removeBreakpoint(node: DevtoolsSignalGraphNode) { + const frame = this.frameManager.selectedFrame(); + this.appOperations.removeSignalBreakpoint( + { + element: this.signalGraph.element()!, + signalId: node.id, + }, + frame!, + ); + this.activeBreakpoints.update((set) => { + const newSet = new Set(set); + newSet.delete(node.id); + return newSet; + }); + } + expandCluster(clusterId: string) { this.visualizer().expandCluster(clusterId); } diff --git a/devtools/projects/ng-devtools/src/lib/shared/signal-details/signal-details.component.html b/devtools/projects/ng-devtools/src/lib/shared/signal-details/signal-details.component.html index fb6e55b44e7e..4e7ad7821e2b 100644 --- a/devtools/projects/ng-devtools/src/lib/shared/signal-details/signal-details.component.html +++ b/devtools/projects/ng-devtools/src/lib/shared/signal-details/signal-details.component.html @@ -16,6 +16,17 @@ > code + } @else if (isClusterNode) {