@marsaude/devtools-shell 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1645,6 +1645,282 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1645
1645
  type: Injectable
1646
1646
  }] });
1647
1647
 
1648
+ /** Ring-buffer cap so a long QA session never grows unbounded. */
1649
+ const MAX_ENTRIES$1 = 300;
1650
+ /**
1651
+ * In-memory telemetry of every HTTP call to the API host (the one in
1652
+ * `apiBaseUrl`). Patches `fetch` and `XMLHttpRequest` once so it captures BOTH
1653
+ * the host app's requests and the package's own — independent of which
1654
+ * HttpClient/interceptor issued them. Nothing is persisted: a page reload
1655
+ * clears the log.
1656
+ */
1657
+ class DevtoolsTelemetryService {
1658
+ constructor() {
1659
+ this.config = inject(DEVTOOLS_CONFIG);
1660
+ this._entries = signal([], ...(ngDevMode ? [{ debugName: "_entries" }] : []));
1661
+ this.entries = this._entries.asReadonly();
1662
+ this.installed = false;
1663
+ this.seq = 0;
1664
+ this.origin = this.deriveOrigin();
1665
+ }
1666
+ clear() {
1667
+ this._entries.set([]);
1668
+ }
1669
+ /** Monkeypatch fetch + XHR once. Safe to call multiple times. */
1670
+ install() {
1671
+ if (this.installed || typeof window === 'undefined')
1672
+ return;
1673
+ this.installed = true;
1674
+ this.patchFetch();
1675
+ this.patchXhr();
1676
+ }
1677
+ // ---- capture helpers --------------------------------------------------
1678
+ deriveOrigin() {
1679
+ try {
1680
+ return new URL(this.config.apiBaseUrl).origin;
1681
+ }
1682
+ catch {
1683
+ return this.config.apiBaseUrl;
1684
+ }
1685
+ }
1686
+ matches(url) {
1687
+ try {
1688
+ return new URL(url, window.location.href).origin === this.origin;
1689
+ }
1690
+ catch {
1691
+ return url.includes(this.origin);
1692
+ }
1693
+ }
1694
+ parseParams(url) {
1695
+ try {
1696
+ const out = {};
1697
+ new URL(url, window.location.href).searchParams.forEach((v, k) => (out[k] = v));
1698
+ return out;
1699
+ }
1700
+ catch {
1701
+ return {};
1702
+ }
1703
+ }
1704
+ tryParse(text) {
1705
+ if (text == null || text === '')
1706
+ return null;
1707
+ try {
1708
+ return JSON.parse(text);
1709
+ }
1710
+ catch {
1711
+ return text;
1712
+ }
1713
+ }
1714
+ normalizeBody(body) {
1715
+ if (body == null)
1716
+ return null;
1717
+ if (typeof body === 'string')
1718
+ return this.tryParse(body);
1719
+ if (typeof FormData !== 'undefined' && body instanceof FormData) {
1720
+ const out = {};
1721
+ body.forEach((v, k) => (out[k] = v instanceof File ? `[File ${v.name}]` : v));
1722
+ return out;
1723
+ }
1724
+ if (typeof Blob !== 'undefined' && body instanceof Blob)
1725
+ return `[Blob ${body.size}b]`;
1726
+ return body;
1727
+ }
1728
+ begin(method, url, body, source) {
1729
+ const entry = {
1730
+ id: ++this.seq,
1731
+ startedAt: Date.now(),
1732
+ method,
1733
+ url,
1734
+ params: this.parseParams(url),
1735
+ requestBody: this.normalizeBody(body),
1736
+ status: null,
1737
+ ok: null,
1738
+ responseBody: null,
1739
+ error: null,
1740
+ durationMs: null,
1741
+ pending: true,
1742
+ source,
1743
+ };
1744
+ this._entries.update((list) => [entry, ...list].slice(0, MAX_ENTRIES$1));
1745
+ return entry;
1746
+ }
1747
+ finish(id, patch) {
1748
+ this._entries.update((list) => list.map((e) => (e.id === id ? { ...e, ...patch, pending: false } : e)));
1749
+ }
1750
+ // ---- patches ----------------------------------------------------------
1751
+ patchFetch() {
1752
+ if (typeof window.fetch !== 'function')
1753
+ return;
1754
+ const orig = window.fetch.bind(window);
1755
+ const self = this;
1756
+ window.fetch = function (input, init) {
1757
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
1758
+ if (!self.matches(url))
1759
+ return orig(input, init);
1760
+ const method = (init?.method || (input instanceof Request ? input.method : 'GET')).toUpperCase();
1761
+ const entry = self.begin(method, url, init?.body, 'fetch');
1762
+ const t0 = performance.now();
1763
+ return orig(input, init).then((res) => {
1764
+ const durationMs = Math.round(performance.now() - t0);
1765
+ res
1766
+ .clone()
1767
+ .text()
1768
+ .then((text) => self.finish(entry.id, {
1769
+ status: res.status,
1770
+ ok: res.ok,
1771
+ responseBody: self.tryParse(text),
1772
+ error: res.ok ? null : `HTTP ${res.status}`,
1773
+ durationMs,
1774
+ }))
1775
+ .catch(() => self.finish(entry.id, { status: res.status, ok: res.ok, durationMs }));
1776
+ return res;
1777
+ }, (err) => {
1778
+ self.finish(entry.id, { error: String(err?.message ?? err), ok: false, durationMs: Math.round(performance.now() - t0) });
1779
+ throw err;
1780
+ });
1781
+ };
1782
+ }
1783
+ patchXhr() {
1784
+ if (typeof XMLHttpRequest === 'undefined')
1785
+ return;
1786
+ const proto = XMLHttpRequest.prototype;
1787
+ const origOpen = proto.open;
1788
+ const origSend = proto.send;
1789
+ const self = this;
1790
+ proto.open = function (method, url, ...rest) {
1791
+ this.__dtMeta = { method: String(method).toUpperCase(), url: String(url) };
1792
+ return origOpen.call(this, method, url, ...rest);
1793
+ };
1794
+ proto.send = function (body) {
1795
+ const meta = this.__dtMeta;
1796
+ if (meta && self.matches(meta.url)) {
1797
+ const entry = self.begin(meta.method, meta.url, body, 'xhr');
1798
+ const t0 = performance.now();
1799
+ this.addEventListener('loadend', () => {
1800
+ const durationMs = Math.round(performance.now() - t0);
1801
+ const status = this.status;
1802
+ if (status === 0) {
1803
+ self.finish(entry.id, { error: 'Network error / aborted', ok: false, durationMs });
1804
+ return;
1805
+ }
1806
+ const ok = status >= 200 && status < 300;
1807
+ let responseBody = null;
1808
+ try {
1809
+ const type = this.responseType;
1810
+ if (type === '' || type === 'text')
1811
+ responseBody = self.tryParse(this.responseText);
1812
+ else if (type === 'json')
1813
+ responseBody = this.response;
1814
+ else
1815
+ responseBody = `[${type}]`;
1816
+ }
1817
+ catch {
1818
+ /* ignore */
1819
+ }
1820
+ self.finish(entry.id, { status, ok, responseBody, error: ok ? null : `HTTP ${status}`, durationMs });
1821
+ });
1822
+ }
1823
+ return origSend.call(this, body);
1824
+ };
1825
+ }
1826
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1827
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTelemetryService }); }
1828
+ }
1829
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTelemetryService, decorators: [{
1830
+ type: Injectable
1831
+ }] });
1832
+
1833
+ /** Ring-buffer cap so a noisy session never grows unbounded. */
1834
+ const MAX_ENTRIES = 300;
1835
+ /**
1836
+ * In-memory log of runtime JS/TS errors: uncaught exceptions (`window.error`),
1837
+ * unhandled promise rejections, and anything sent to `console.error` (which is
1838
+ * where Angular's default ErrorHandler reports zone-caught errors). Captures
1839
+ * the host app's errors and the package's alike. Nothing is persisted — a page
1840
+ * reload clears it.
1841
+ */
1842
+ class DevtoolsErrorsService {
1843
+ constructor() {
1844
+ this._entries = signal([], ...(ngDevMode ? [{ debugName: "_entries" }] : []));
1845
+ this.entries = this._entries.asReadonly();
1846
+ this.installed = false;
1847
+ this.seq = 0;
1848
+ }
1849
+ clear() {
1850
+ this._entries.set([]);
1851
+ }
1852
+ /** Attach the listeners + patch console.error once. Safe to call repeatedly. */
1853
+ install() {
1854
+ if (this.installed || typeof window === 'undefined')
1855
+ return;
1856
+ this.installed = true;
1857
+ window.addEventListener('error', (e) => {
1858
+ // Skip resource-load errors (img/script/link) — those carry no error/message.
1859
+ if (!e.error && !e.message)
1860
+ return;
1861
+ this.push('error', e.message || String(e.error), e.error?.stack ?? null, this.loc(e.filename, e.lineno, e.colno));
1862
+ });
1863
+ window.addEventListener('unhandledrejection', (e) => {
1864
+ const reason = e.reason;
1865
+ const message = reason instanceof Error ? reason.message : this.stringify(reason);
1866
+ this.push('rejection', message, reason?.stack ?? null, null);
1867
+ });
1868
+ const orig = console.error.bind(console);
1869
+ const self = this;
1870
+ console.error = function (...args) {
1871
+ orig(...args);
1872
+ try {
1873
+ self.captureConsole(args);
1874
+ }
1875
+ catch {
1876
+ /* never let telemetry break logging */
1877
+ }
1878
+ };
1879
+ }
1880
+ // ---- helpers ----------------------------------------------------------
1881
+ captureConsole(args) {
1882
+ const errArg = args.find((a) => a instanceof Error);
1883
+ const message = args
1884
+ .map((a) => (a instanceof Error ? a.message : typeof a === 'string' ? a : this.stringify(a)))
1885
+ .join(' ')
1886
+ .trim();
1887
+ if (!message)
1888
+ return;
1889
+ this.push('console', message, errArg?.stack ?? null, null);
1890
+ }
1891
+ loc(file, line, col) {
1892
+ if (!file)
1893
+ return null;
1894
+ return `${file}${line != null ? ':' + line : ''}${col != null ? ':' + col : ''}`;
1895
+ }
1896
+ stringify(v) {
1897
+ if (v == null)
1898
+ return String(v);
1899
+ if (typeof v === 'string')
1900
+ return v;
1901
+ try {
1902
+ return JSON.stringify(v);
1903
+ }
1904
+ catch {
1905
+ return String(v);
1906
+ }
1907
+ }
1908
+ push(kind, message, stack, source) {
1909
+ // Collapse an immediate duplicate (e.g. Angular ErrorHandler -> console.error
1910
+ // right after a window 'error' for the same throw).
1911
+ const last = this._entries()[0];
1912
+ if (last && last.message === message && last.stack === stack)
1913
+ return;
1914
+ const entry = { id: ++this.seq, at: Date.now(), kind, message, stack, source };
1915
+ this._entries.update((list) => [entry, ...list].slice(0, MAX_ENTRIES));
1916
+ }
1917
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsErrorsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1918
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsErrorsService }); }
1919
+ }
1920
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsErrorsService, decorators: [{
1921
+ type: Injectable
1922
+ }] });
1923
+
1648
1924
  /**
1649
1925
  * Shared inline-style objects for the DevTools domain panels. Bound via
1650
1926
  * `[style]="UI.x"` so there are no view-encapsulation surprises when the panels
@@ -3038,6 +3314,308 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
3038
3314
  }]
3039
3315
  }] });
3040
3316
 
3317
+ /**
3318
+ * Live, in-memory log of every API call (host app + package). Click a row to
3319
+ * inspect params/body/response/error. Cleared by a page reload or the button.
3320
+ */
3321
+ class TelemetryPanelComponent {
3322
+ constructor() {
3323
+ this.UI = UI;
3324
+ this.telemetry = inject(DevtoolsTelemetryService);
3325
+ this.entries = this.telemetry.entries;
3326
+ this.count = computed(() => this.entries().length, ...(ngDevMode ? [{ debugName: "count" }] : []));
3327
+ this.expanded = signal(null, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
3328
+ this.badge = {
3329
+ fontFamily: "'JetBrains Mono',monospace",
3330
+ fontSize: '9.5px',
3331
+ fontWeight: '700',
3332
+ letterSpacing: '.03em',
3333
+ padding: '2px 6px',
3334
+ borderRadius: '6px',
3335
+ background: '#23252f',
3336
+ color: '#c4c7d0',
3337
+ flex: 'none',
3338
+ };
3339
+ }
3340
+ toggle(id) {
3341
+ this.expanded.update((cur) => (cur === id ? null : id));
3342
+ }
3343
+ clear() {
3344
+ this.telemetry.clear();
3345
+ this.expanded.set(null);
3346
+ }
3347
+ path(url) {
3348
+ try {
3349
+ const u = new URL(url, window.location.href);
3350
+ return u.pathname + u.search;
3351
+ }
3352
+ catch {
3353
+ return url;
3354
+ }
3355
+ }
3356
+ hasParams(e) {
3357
+ return Object.keys(e.params).length > 0;
3358
+ }
3359
+ json(v) {
3360
+ try {
3361
+ return typeof v === 'string' ? v : JSON.stringify(v, null, 2);
3362
+ }
3363
+ catch {
3364
+ return String(v);
3365
+ }
3366
+ }
3367
+ statusColor(e) {
3368
+ if (e.pending)
3369
+ return '#ffc454';
3370
+ if (e.error || (e.status ?? 0) >= 400)
3371
+ return '#ff6b6b';
3372
+ return '#1fbe7e';
3373
+ }
3374
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TelemetryPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3375
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: TelemetryPanelComponent, isStandalone: true, selector: "devtools-telemetry-panel", ngImport: i0, template: `
3376
+ <p [style]="UI.intro" style="margin-bottom:10px">
3377
+ Todas as chamadas à API ({{ count() }}). Em memória — o reload limpa.
3378
+ </p>
3379
+ <button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
3380
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
3381
+ Limpar log
3382
+ </button>
3383
+
3384
+ @if (!count()) {
3385
+ <p [style]="UI.mono">Nenhuma requisição capturada ainda.</p>
3386
+ }
3387
+
3388
+ @for (e of entries(); track e.id) {
3389
+ <div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px" (click)="toggle(e.id)">
3390
+ <div style="display:flex;align-items:center;gap:8px">
3391
+ <span [style]="badge">{{ e.method }}</span>
3392
+ <span style="font-size:11px;font-weight:700;min-width:32px" [style.color]="statusColor(e)">
3393
+ {{ e.pending ? '···' : (e.status ?? 'ERR') }}
3394
+ </span>
3395
+ <span [style]="UI.mono" style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
3396
+ {{ path(e.url) }}
3397
+ </span>
3398
+ @if (e.durationMs != null) {
3399
+ <span [style]="UI.mono" style="white-space:nowrap;color:#6a6e7b">{{ e.durationMs }}ms</span>
3400
+ }
3401
+ </div>
3402
+
3403
+ @if (expanded() === e.id) {
3404
+ <div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
3405
+ <div [style]="UI.label">URL</div>
3406
+ <pre [style]="UI.pre">{{ e.url }}</pre>
3407
+
3408
+ @if (hasParams(e)) {
3409
+ <div [style]="UI.label">Query params</div>
3410
+ <pre [style]="UI.pre">{{ json(e.params) }}</pre>
3411
+ }
3412
+ @if (e.requestBody !== null) {
3413
+ <div [style]="UI.label">Request body</div>
3414
+ <pre [style]="UI.pre">{{ json(e.requestBody) }}</pre>
3415
+ }
3416
+ @if (e.error) {
3417
+ <div [style]="UI.label" style="color:#ff7676">Erro</div>
3418
+ <pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.error }}</pre>
3419
+ }
3420
+ @if (e.responseBody !== null) {
3421
+ <div [style]="UI.label">Response</div>
3422
+ <pre [style]="UI.pre">{{ json(e.responseBody) }}</pre>
3423
+ }
3424
+ </div>
3425
+ }
3426
+ </div>
3427
+ }
3428
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3429
+ }
3430
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TelemetryPanelComponent, decorators: [{
3431
+ type: Component,
3432
+ args: [{
3433
+ selector: 'devtools-telemetry-panel',
3434
+ standalone: true,
3435
+ changeDetection: ChangeDetectionStrategy.OnPush,
3436
+ template: `
3437
+ <p [style]="UI.intro" style="margin-bottom:10px">
3438
+ Todas as chamadas à API ({{ count() }}). Em memória — o reload limpa.
3439
+ </p>
3440
+ <button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
3441
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
3442
+ Limpar log
3443
+ </button>
3444
+
3445
+ @if (!count()) {
3446
+ <p [style]="UI.mono">Nenhuma requisição capturada ainda.</p>
3447
+ }
3448
+
3449
+ @for (e of entries(); track e.id) {
3450
+ <div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px" (click)="toggle(e.id)">
3451
+ <div style="display:flex;align-items:center;gap:8px">
3452
+ <span [style]="badge">{{ e.method }}</span>
3453
+ <span style="font-size:11px;font-weight:700;min-width:32px" [style.color]="statusColor(e)">
3454
+ {{ e.pending ? '···' : (e.status ?? 'ERR') }}
3455
+ </span>
3456
+ <span [style]="UI.mono" style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
3457
+ {{ path(e.url) }}
3458
+ </span>
3459
+ @if (e.durationMs != null) {
3460
+ <span [style]="UI.mono" style="white-space:nowrap;color:#6a6e7b">{{ e.durationMs }}ms</span>
3461
+ }
3462
+ </div>
3463
+
3464
+ @if (expanded() === e.id) {
3465
+ <div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
3466
+ <div [style]="UI.label">URL</div>
3467
+ <pre [style]="UI.pre">{{ e.url }}</pre>
3468
+
3469
+ @if (hasParams(e)) {
3470
+ <div [style]="UI.label">Query params</div>
3471
+ <pre [style]="UI.pre">{{ json(e.params) }}</pre>
3472
+ }
3473
+ @if (e.requestBody !== null) {
3474
+ <div [style]="UI.label">Request body</div>
3475
+ <pre [style]="UI.pre">{{ json(e.requestBody) }}</pre>
3476
+ }
3477
+ @if (e.error) {
3478
+ <div [style]="UI.label" style="color:#ff7676">Erro</div>
3479
+ <pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.error }}</pre>
3480
+ }
3481
+ @if (e.responseBody !== null) {
3482
+ <div [style]="UI.label">Response</div>
3483
+ <pre [style]="UI.pre">{{ json(e.responseBody) }}</pre>
3484
+ }
3485
+ </div>
3486
+ }
3487
+ </div>
3488
+ }
3489
+ `,
3490
+ }]
3491
+ }] });
3492
+
3493
+ /**
3494
+ * Live, in-memory log of runtime JS/TS errors (uncaught, unhandled rejections,
3495
+ * console.error). Click a row to see the full stack. Cleared by a reload.
3496
+ */
3497
+ class ErrorsPanelComponent {
3498
+ constructor() {
3499
+ this.UI = UI;
3500
+ this.errors = inject(DevtoolsErrorsService);
3501
+ this.entries = this.errors.entries;
3502
+ this.count = computed(() => this.entries().length, ...(ngDevMode ? [{ debugName: "count" }] : []));
3503
+ this.expanded = signal(null, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
3504
+ this.badge = {
3505
+ fontFamily: "'JetBrains Mono',monospace",
3506
+ fontSize: '9.5px',
3507
+ fontWeight: '700',
3508
+ letterSpacing: '.03em',
3509
+ padding: '2px 6px',
3510
+ borderRadius: '6px',
3511
+ background: '#2a2024',
3512
+ flex: 'none',
3513
+ };
3514
+ }
3515
+ toggle(id) {
3516
+ this.expanded.update((cur) => (cur === id ? null : id));
3517
+ }
3518
+ clear() {
3519
+ this.errors.clear();
3520
+ this.expanded.set(null);
3521
+ }
3522
+ kindLabel(kind) {
3523
+ return kind === 'rejection' ? 'PROMISE' : kind === 'console' ? 'CONSOLE' : 'ERROR';
3524
+ }
3525
+ kindColor(kind) {
3526
+ return kind === 'console' ? '#ffb454' : '#ff6b6b';
3527
+ }
3528
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ErrorsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3529
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ErrorsPanelComponent, isStandalone: true, selector: "devtools-errors-panel", ngImport: i0, template: `
3530
+ <p [style]="UI.intro" style="margin-bottom:10px">
3531
+ Erros de runtime JS/TS ({{ count() }}). Em memória — o reload limpa.
3532
+ </p>
3533
+ <button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
3534
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
3535
+ Limpar log
3536
+ </button>
3537
+
3538
+ @if (!count()) {
3539
+ <p [style]="UI.mono">Nenhum erro capturado. 🎉</p>
3540
+ }
3541
+
3542
+ @for (e of entries(); track e.id) {
3543
+ <div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px;border-color:#3a2a2a" (click)="toggle(e.id)">
3544
+ <div style="display:flex;align-items:center;gap:8px">
3545
+ <span [style]="badge" [style.color]="kindColor(e.kind)">{{ kindLabel(e.kind) }}</span>
3546
+ <span style="flex:1;min-width:0;font-size:12.5px;color:#f0d6d6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
3547
+ {{ e.message }}
3548
+ </span>
3549
+ </div>
3550
+
3551
+ @if (expanded() === e.id) {
3552
+ <div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
3553
+ <div [style]="UI.label">Mensagem</div>
3554
+ <pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.message }}</pre>
3555
+
3556
+ @if (e.source) {
3557
+ <div [style]="UI.label">Origem</div>
3558
+ <pre [style]="UI.pre">{{ e.source }}</pre>
3559
+ }
3560
+ @if (e.stack) {
3561
+ <div [style]="UI.label">Stack</div>
3562
+ <pre [style]="UI.pre">{{ e.stack }}</pre>
3563
+ }
3564
+ </div>
3565
+ }
3566
+ </div>
3567
+ }
3568
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3569
+ }
3570
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ErrorsPanelComponent, decorators: [{
3571
+ type: Component,
3572
+ args: [{
3573
+ selector: 'devtools-errors-panel',
3574
+ standalone: true,
3575
+ changeDetection: ChangeDetectionStrategy.OnPush,
3576
+ template: `
3577
+ <p [style]="UI.intro" style="margin-bottom:10px">
3578
+ Erros de runtime JS/TS ({{ count() }}). Em memória — o reload limpa.
3579
+ </p>
3580
+ <button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
3581
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
3582
+ Limpar log
3583
+ </button>
3584
+
3585
+ @if (!count()) {
3586
+ <p [style]="UI.mono">Nenhum erro capturado. 🎉</p>
3587
+ }
3588
+
3589
+ @for (e of entries(); track e.id) {
3590
+ <div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px;border-color:#3a2a2a" (click)="toggle(e.id)">
3591
+ <div style="display:flex;align-items:center;gap:8px">
3592
+ <span [style]="badge" [style.color]="kindColor(e.kind)">{{ kindLabel(e.kind) }}</span>
3593
+ <span style="flex:1;min-width:0;font-size:12.5px;color:#f0d6d6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
3594
+ {{ e.message }}
3595
+ </span>
3596
+ </div>
3597
+
3598
+ @if (expanded() === e.id) {
3599
+ <div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
3600
+ <div [style]="UI.label">Mensagem</div>
3601
+ <pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.message }}</pre>
3602
+
3603
+ @if (e.source) {
3604
+ <div [style]="UI.label">Origem</div>
3605
+ <pre [style]="UI.pre">{{ e.source }}</pre>
3606
+ }
3607
+ @if (e.stack) {
3608
+ <div [style]="UI.label">Stack</div>
3609
+ <pre [style]="UI.pre">{{ e.stack }}</pre>
3610
+ }
3611
+ </div>
3612
+ }
3613
+ </div>
3614
+ }
3615
+ `,
3616
+ }]
3617
+ }] });
3618
+
3041
3619
  const BUILTIN_ACTIONS = [
3042
3620
  { id: 'admin', label: 'Admin', icon: 'shield_person', content: AdminLoginPanelComponent, order: 0 },
3043
3621
  { id: 'app-login', label: 'Login App', icon: 'login', content: LoginPanelComponent, order: 1 },
@@ -3047,6 +3625,8 @@ const BUILTIN_ACTIONS = [
3047
3625
  { id: 'exams', label: 'Exames', icon: 'biotech', content: ExamsPanelComponent, order: 5 },
3048
3626
  { id: 'data', label: 'Dados', icon: 'badge', content: DataPanelComponent, order: 6 },
3049
3627
  { id: 'copy', label: 'Copiar', icon: 'content_copy', content: CopyPanelComponent, order: 7 },
3628
+ { id: 'telemetry', label: 'Telemetria', icon: 'monitoring', content: TelemetryPanelComponent, order: 8 },
3629
+ { id: 'errors', label: 'Erros', icon: 'bug_report', content: ErrorsPanelComponent, order: 9 },
3050
3630
  ];
3051
3631
  /**
3052
3632
  * Mount the self-contained DevTools. Add to `bootstrapApplication` providers or
@@ -3110,11 +3690,18 @@ function mountIsolated(parent, config, actions, shell) {
3110
3690
  DevtoolsApiService,
3111
3691
  DevtoolsAuthService,
3112
3692
  DevtoolsSessionService,
3693
+ DevtoolsTelemetryService,
3694
+ DevtoolsErrorsService,
3113
3695
  DevtoolsPositionService,
3114
3696
  DevtoolsToastService,
3115
3697
  ], parent);
3116
- // Consume our own Google OAuth return (guarded) to finish admin login.
3117
- runInInjectionContext(injector, () => inject(DevtoolsAuthService).handleGoogleReturn());
3698
+ runInInjectionContext(injector, () => {
3699
+ // Start capturing API traffic + runtime errors ASAP, then consume our own
3700
+ // Google OAuth return (guarded) to finish admin login.
3701
+ inject(DevtoolsTelemetryService).install();
3702
+ inject(DevtoolsErrorsService).install();
3703
+ inject(DevtoolsAuthService).handleGoogleReturn();
3704
+ });
3118
3705
  const appRef = parent.get(ApplicationRef);
3119
3706
  const host = document.createElement('div');
3120
3707
  host.setAttribute('data-devtools-shell-host', '');