@dasasian/firebase-structured-logger 0.1.0

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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/dist/client/breadcrumbs.d.ts +15 -0
  4. package/dist/client/breadcrumbs.d.ts.map +1 -0
  5. package/dist/client/breadcrumbs.js +52 -0
  6. package/dist/client/breadcrumbs.js.map +1 -0
  7. package/dist/client/errorHandler.d.ts +5 -0
  8. package/dist/client/errorHandler.d.ts.map +1 -0
  9. package/dist/client/errorHandler.js +19 -0
  10. package/dist/client/errorHandler.js.map +1 -0
  11. package/dist/client/index.d.ts +6 -0
  12. package/dist/client/index.d.ts.map +1 -0
  13. package/dist/client/index.js +16 -0
  14. package/dist/client/index.js.map +1 -0
  15. package/dist/client/logger.d.ts +40 -0
  16. package/dist/client/logger.d.ts.map +1 -0
  17. package/dist/client/logger.js +199 -0
  18. package/dist/client/logger.js.map +1 -0
  19. package/dist/client/rateLimiter.d.ts +12 -0
  20. package/dist/client/rateLimiter.d.ts.map +1 -0
  21. package/dist/client/rateLimiter.js +80 -0
  22. package/dist/client/rateLimiter.js.map +1 -0
  23. package/dist/functions/index.d.ts +12 -0
  24. package/dist/functions/index.d.ts.map +1 -0
  25. package/dist/functions/index.js +21 -0
  26. package/dist/functions/index.js.map +1 -0
  27. package/dist/functions/logHandler.d.ts +27 -0
  28. package/dist/functions/logHandler.d.ts.map +1 -0
  29. package/dist/functions/logHandler.js +137 -0
  30. package/dist/functions/logHandler.js.map +1 -0
  31. package/dist/functions/logger.d.ts +32 -0
  32. package/dist/functions/logger.d.ts.map +1 -0
  33. package/dist/functions/logger.js +253 -0
  34. package/dist/functions/logger.js.map +1 -0
  35. package/dist/functions/requestLogger.d.ts +30 -0
  36. package/dist/functions/requestLogger.d.ts.map +1 -0
  37. package/dist/functions/requestLogger.js +45 -0
  38. package/dist/functions/requestLogger.js.map +1 -0
  39. package/dist/functions/sourceMapCache.d.ts +9 -0
  40. package/dist/functions/sourceMapCache.d.ts.map +1 -0
  41. package/dist/functions/sourceMapCache.js +107 -0
  42. package/dist/functions/sourceMapCache.js.map +1 -0
  43. package/dist/functions/symbolicate.d.ts +31 -0
  44. package/dist/functions/symbolicate.d.ts.map +1 -0
  45. package/dist/functions/symbolicate.js +112 -0
  46. package/dist/functions/symbolicate.js.map +1 -0
  47. package/dist/shared/types.d.ts +36 -0
  48. package/dist/shared/types.d.ts.map +1 -0
  49. package/dist/shared/types.js +3 -0
  50. package/dist/shared/types.js.map +1 -0
  51. package/dist/tools/index.d.ts +3 -0
  52. package/dist/tools/index.d.ts.map +1 -0
  53. package/dist/tools/index.js +152 -0
  54. package/dist/tools/index.js.map +1 -0
  55. package/dist/tools/installSkills.d.ts +6 -0
  56. package/dist/tools/installSkills.d.ts.map +1 -0
  57. package/dist/tools/installSkills.js +89 -0
  58. package/dist/tools/installSkills.js.map +1 -0
  59. package/dist/tools/packDeploy.d.ts +15 -0
  60. package/dist/tools/packDeploy.d.ts.map +1 -0
  61. package/dist/tools/packDeploy.js +148 -0
  62. package/dist/tools/packDeploy.js.map +1 -0
  63. package/dist/tools/uploadSourceMaps.d.ts +9 -0
  64. package/dist/tools/uploadSourceMaps.d.ts.map +1 -0
  65. package/dist/tools/uploadSourceMaps.js +120 -0
  66. package/dist/tools/uploadSourceMaps.js.map +1 -0
  67. package/package.json +103 -0
  68. package/skills/logs/SKILL.md +185 -0
  69. package/skills/query-logs/SKILL.md +93 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dasasian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/dasasian/firebase-structured-logger/main/assets/hero.svg" alt="firebase-structured-logger — structured logging for Firebase apps" width="900">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@dasasian/firebase-structured-logger"><img alt="npm" src="https://img.shields.io/npm/v/@dasasian/firebase-structured-logger?style=flat-square&color=235a9b"></a>
7
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-235a9b?style=flat-square"></a>
8
+ <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-strict-3178c6?style=flat-square">
9
+ <img alt="Node 18+" src="https://img.shields.io/badge/node-18%2B-5fa04e?style=flat-square">
10
+ </p>
11
+
12
+ # @dasasian/firebase-structured-logger
13
+
14
+ One logging pipeline for a Firebase app, from the browser to Google Cloud Logging: frontend events go to a Cloud Function, production stack traces are **symbolicated** against your source maps, and everything is written as **structured, queryable entries**. Ships a client logger, a Cloud Functions logger, and an `fsl` CLI.
15
+
16
+ ## Why
17
+
18
+ - **Symbolicated frontend errors** — minified production traces resolve back to `File.tsx:line:col` using source maps you upload at deploy time. No more chasing `app-4f2a.js:1:98432`.
19
+ - **Structured entries** — severity, labels, user / session / screen context, breadcrumbs, and file attachments, all queryable in Cloud Logging.
20
+ - **One API across surfaces** — the same `logger.info/warning/error/debug` shape on the client and in Cloud Functions.
21
+ - **Local dev loop** — the Functions emulator writes JSONL you can `tail` or query without deploying — and query from Claude with the companion [**firebase-mcp-server**](https://github.com/dasasian/firebase-mcp-server).
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ # frontend (project root)
27
+ npm install @dasasian/firebase-structured-logger
28
+
29
+ # Cloud Functions
30
+ cd functions && npm install @dasasian/firebase-structured-logger
31
+ ```
32
+
33
+ Ships ESM with three entry points — `/client`, `/functions`, `/tools` — plus the `fsl` CLI. `firebase`, `firebase-admin`, and `firebase-functions` are optional peer dependencies (bring your own versions).
34
+
35
+ ## Quick start
36
+
37
+ **1. Initialize the client** — at your app entry (e.g. `src/main.tsx`), before any logging:
38
+
39
+ ```ts
40
+ import { initLogger, setupGlobalErrorHandler } from '@dasasian/firebase-structured-logger/client'
41
+ import { httpsCallable } from 'firebase/functions'
42
+ import { functions } from './config/firebase'
43
+
44
+ export const logger = initLogger({
45
+ appId: 'my-app',
46
+ releaseId: import.meta.env.VITE_RELEASE_ID ?? 'dev',
47
+ logFunction: httpsCallable(functions, 'logFrontendEvent'),
48
+ })
49
+
50
+ setupGlobalErrorHandler() // capture uncaught errors + unhandled rejections
51
+ ```
52
+
53
+ **2. Add the Cloud Function** — in `functions/src/index.ts`:
54
+
55
+ ```ts
56
+ import { initLogger, createClientLogFunction } from '@dasasian/firebase-structured-logger/functions'
57
+
58
+ initLogger({ appId: 'my-app' })
59
+
60
+ export const logFrontendEvent = createClientLogFunction({
61
+ bucketName: 'my-app.firebasestorage.app', // Storage bucket holding source maps
62
+ })
63
+ ```
64
+
65
+ **3. Log** — anywhere in the frontend:
66
+
67
+ ```ts
68
+ logger.info('checkout started', { orderId })
69
+ logger.error(err, { screen: 'camera' }, context, { photo: blob }) // attachments optional
70
+ ```
71
+
72
+ Debug logs are suppressed in production automatically. That's the happy path — the rest of this doc covers source-map symbolication, user context, the CLI, and the local emulator loop.
73
+
74
+ ---
75
+
76
+ ## How it works
77
+
78
+ ```
79
+ Browser ──logFrontendEvent()──▶ Cloud Function ──symbolicate──▶ Cloud Logging
80
+ (client) httpsCallable (functions) (source maps) (structured)
81
+ ```
82
+
83
+ - The **client** batches structured events and calls your `logFrontendEvent` Cloud Function.
84
+ - The **function** symbolicates any stack trace against source maps in Cloud Storage, then writes a structured entry (in production) or a local JSONL file (in the emulator).
85
+ - You query the result in **Cloud Logging**, or locally from Claude via **[firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server)** (`@dasasian/firebase-mcp-server`).
86
+
87
+ ## Full setup
88
+
89
+ ### Prerequisites
90
+
91
+ - A `functions/` directory (`firebase init functions`, TypeScript) referenced by `firebase.json`.
92
+ - **Firebase Storage enabled** (source maps are uploaded there): Firebase console → **Storage → Get started**.
93
+ - A frontend build that emits source maps. The `fsl` source-map tooling assumes **Vite**.
94
+
95
+ ### 1. `.gitignore`
96
+
97
+ ```
98
+ functions/logs/*.jsonl
99
+ ```
100
+
101
+ The `logs/` directory holds local JSONL from the emulator — track the directory, not the files:
102
+
103
+ ```bash
104
+ mkdir -p functions/logs && touch functions/logs/.gitkeep
105
+ ```
106
+
107
+ ### 2. Wire the deploy script
108
+
109
+ Upload source maps (and strip them from the hosting bundle) as part of deploy. Merge into your root `package.json` scripts — keep any existing flags like `--project`:
110
+
111
+ ```json
112
+ "deploy": "export VITE_RELEASE_ID=$(git rev-parse --short HEAD) && npm run build && npx fsl upload-sourcemaps --functions=./functions --embed-sourcemaps && firebase deploy"
113
+ ```
114
+
115
+ `fsl upload-sourcemaps` reads the bucket from `VITE_FIREBASE_STORAGE_BUCKET` (or `FIREBASE_STORAGE_BUCKET`) after loading `.env.local`. It uploads source maps to Cloud Storage, embeds a copy in `functions/sourcemaps/current/` for fast lookup, and deletes them from `dist/` so they are **not** served to browsers.
116
+
117
+ > **`VITE_RELEASE_ID`** ties a build to its source maps — the client tags every entry with it, and `upload-sourcemaps` stores maps under the matching path. Use the same value in both places (the deploy script above sets it once from the git SHA). Locally it defaults to `'dev'`, and no maps are uploaded — symbolication isn't needed in development.
118
+
119
+ ### 3. Typed labels + user context
120
+
121
+ ```ts
122
+ interface MyAppLabels {
123
+ organizationId?: string
124
+ itemId?: string
125
+ // whatever domain entities are relevant
126
+ }
127
+
128
+ export const logger = initLogger<MyAppLabels>({ /* … */ })
129
+
130
+ logger.setUser(uid, { /* app labels */ }) // on sign in
131
+ logger.clearUser() // on sign out
132
+ logger.setScreen('checkout') // on navigation
133
+ ```
134
+
135
+ ### 4. Run the emulator (local capture)
136
+
137
+ Add a `serve` script to `functions/package.json`:
138
+
139
+ ```json
140
+ "serve": "npm run build && firebase emulators:start --only functions"
141
+ ```
142
+
143
+ The emulator writes entries to `DEV_LOG_DIR` (e.g. `functions/logs/dev.jsonl`), so you can inspect logs without deploying — and point **[firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server)** at that same file to query your dev logs from Claude.
144
+
145
+ ## Client API
146
+
147
+ ```ts
148
+ logger.error(error, labels?, context?, attachments?) // attachments: Record<string, Blob | File | string>
149
+ logger.info(message, labels?, context?, attachments?)
150
+ logger.warning(message, labels?, context?, attachments?)
151
+ logger.debug(message, labels?, context?, attachments?) // suppressed in production
152
+
153
+ logger.setUser(uid, extraLabels?)
154
+ logger.clearUser()
155
+ logger.setScreen(screen)
156
+ logger.addBreadcrumb(type, name, data?)
157
+ ```
158
+
159
+ **Attachments** (images, snapshots, captured data) upload to GCS at `logAttachments/{logId}/{name}`; the same `logId` is on the entry's `labels.logId`. Add a lifecycle rule to auto-delete `logAttachments/` after N days.
160
+
161
+ ## Functions API
162
+
163
+ ```ts
164
+ import { initLogger, initRequestLogger, logError, logInfo } from '@dasasian/firebase-structured-logger/functions'
165
+
166
+ initLogger({ appId: 'my-app' }) // at module load
167
+
168
+ export const myFunction = onCall(async (request) => {
169
+ initRequestLogger(request, { functionName: 'myFunction' }) // seeds userId + functionName
170
+ logInfo('started')
171
+ })
172
+ ```
173
+
174
+ Backend log methods also accept an optional `attachments` (`Record<string, string | Buffer>`).
175
+
176
+ ## CLI (`fsl`)
177
+
178
+ ```bash
179
+ # Upload source maps to Cloud Storage and strip local .map files (run in deploy)
180
+ npx fsl upload-sourcemaps [--bucket=<name>] [--functions=<path>] [--embed-sourcemaps] [--release=<id>]
181
+
182
+ # Install the Claude Code skills into the current project (or --global, --force)
183
+ npx fsl install-skills
184
+ ```
185
+
186
+ > The `fsl pack` / `pack-restore` commands exist only for consuming this package via a local `file:` dependency (they vendor a tgz so `firebase deploy` can bundle it). When you install from npm — as above — you don't need them.
187
+
188
+ ## Skills
189
+
190
+ ```bash
191
+ npx fsl install-skills
192
+ ```
193
+
194
+ | Skill | Description |
195
+ |-------|-------------|
196
+ | `/logs` | Validate logging in a file — error paths, labels, PII, missing `initRequestLogger`, breadcrumbs |
197
+ | `/query-logs` | Query Cloud Logging or local JSONL via [firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server) |
198
+
199
+ ## Local log rotation
200
+
201
+ Logs write to `{logLocalDir}/dev.jsonl`; each emulator start rotates the current file to `dev-{timestamp}.jsonl`, and rotation also happens when the record limit is hit mid-session.
202
+
203
+ | Config | Default | Description |
204
+ |---|---|---|
205
+ | `logLocalDir` | — | Directory for local log files |
206
+ | `logMaxRecordsPerFile` | 2000 | Records per file before rotation |
207
+ | `logMaxRotatedFiles` | 5 | Rotated files to keep |
208
+
209
+ ## Source maps
210
+
211
+ For symbolicated production traces, emit source maps in Vite and upload them at deploy:
212
+
213
+ ```ts
214
+ // vite.config.ts
215
+ build: {
216
+ sourcemap: true,
217
+ commonjsOptions: {
218
+ // Rollup's commonjs plugin doesn't cover file: deps by default
219
+ include: [/firebase-structured-logger/, /node_modules/],
220
+ },
221
+ }
222
+ ```
223
+
224
+ Maps are stored at `gs://<bucket>/sourcemaps/{releaseId}/{filename}.map` and loaded by the Cloud Function during symbolication.
225
+
226
+ ## License
227
+
228
+ MIT © Dasasian
@@ -0,0 +1,15 @@
1
+ import type { BreadcrumbEntry } from '../shared/types';
2
+ export declare function setCurrentScreen(screen: string): void;
3
+ export declare function setActiveActivity(activity: string | undefined): void;
4
+ export declare function getCurrentScreen(): string | undefined;
5
+ export declare function getActiveActivity(): string | undefined;
6
+ export declare function addBreadcrumb(type: BreadcrumbEntry['type'], name: string, data?: Record<string, unknown>): void;
7
+ export declare function getLastBreadcrumbs(count: number): BreadcrumbEntry[];
8
+ export declare function clearBreadcrumbs(): void;
9
+ export declare const bc: {
10
+ action: (name: string, data?: Record<string, unknown>) => void;
11
+ state: (name: string, data?: Record<string, unknown>) => void;
12
+ nav: (screen: string) => void;
13
+ error: (type: string, data?: Record<string, unknown>) => void;
14
+ };
15
+ //# sourceMappingURL=breadcrumbs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breadcrumbs.d.ts","sourceRoot":"","sources":["../../src/client/breadcrumbs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAStD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAGrD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEpE;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAErD;AAED,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,EAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAUN;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,EAAE,CAEnE;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC;AAED,eAAO,MAAM,EAAE;mBACE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;kBACtC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;kBACpC,MAAM;kBACR,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CACtD,CAAA"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bc = void 0;
4
+ exports.setCurrentScreen = setCurrentScreen;
5
+ exports.setActiveActivity = setActiveActivity;
6
+ exports.getCurrentScreen = getCurrentScreen;
7
+ exports.getActiveActivity = getActiveActivity;
8
+ exports.addBreadcrumb = addBreadcrumb;
9
+ exports.getLastBreadcrumbs = getLastBreadcrumbs;
10
+ exports.clearBreadcrumbs = clearBreadcrumbs;
11
+ const MAX_BREADCRUMBS = 50;
12
+ const MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
13
+ let currentScreen;
14
+ let activeActivity;
15
+ let breadcrumbs = [];
16
+ function setCurrentScreen(screen) {
17
+ currentScreen = screen;
18
+ addBreadcrumb('nav', `navigate_${screen}`);
19
+ }
20
+ function setActiveActivity(activity) {
21
+ activeActivity = activity;
22
+ }
23
+ function getCurrentScreen() {
24
+ return currentScreen;
25
+ }
26
+ function getActiveActivity() {
27
+ return activeActivity;
28
+ }
29
+ function addBreadcrumb(type, name, data) {
30
+ const entry = { timestamp: Date.now(), type, name, data };
31
+ breadcrumbs.push(entry);
32
+ const cutoff = Date.now() - MAX_AGE_MS;
33
+ breadcrumbs = breadcrumbs.filter((bc) => bc.timestamp > cutoff);
34
+ if (breadcrumbs.length > MAX_BREADCRUMBS) {
35
+ breadcrumbs = breadcrumbs.slice(breadcrumbs.length - MAX_BREADCRUMBS);
36
+ }
37
+ }
38
+ function getLastBreadcrumbs(count) {
39
+ return breadcrumbs.slice(Math.max(0, breadcrumbs.length - count));
40
+ }
41
+ function clearBreadcrumbs() {
42
+ breadcrumbs = [];
43
+ currentScreen = undefined;
44
+ activeActivity = undefined;
45
+ }
46
+ exports.bc = {
47
+ action: (name, data) => addBreadcrumb('action', name, data),
48
+ state: (name, data) => addBreadcrumb('state', name, data),
49
+ nav: (screen) => setCurrentScreen(screen),
50
+ error: (type, data) => addBreadcrumb('error', type, data),
51
+ };
52
+ //# sourceMappingURL=breadcrumbs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breadcrumbs.js","sourceRoot":"","sources":["../../src/client/breadcrumbs.ts"],"names":[],"mappings":";;;AASA,4CAGC;AAED,8CAEC;AAED,4CAEC;AAED,8CAEC;AAED,sCAcC;AAED,gDAEC;AAED,4CAIC;AAhDD,MAAM,eAAe,GAAG,EAAE,CAAA;AAC1B,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,YAAY;AAE7C,IAAI,aAAiC,CAAA;AACrC,IAAI,cAAkC,CAAA;AACtC,IAAI,WAAW,GAAsB,EAAE,CAAA;AAEvC,SAAgB,gBAAgB,CAAC,MAAc;IAC7C,aAAa,GAAG,MAAM,CAAA;IACtB,aAAa,CAAC,KAAK,EAAE,YAAY,MAAM,EAAE,CAAC,CAAA;AAC5C,CAAC;AAED,SAAgB,iBAAiB,CAAC,QAA4B;IAC5D,cAAc,GAAG,QAAQ,CAAA;AAC3B,CAAC;AAED,SAAgB,gBAAgB;IAC9B,OAAO,aAAa,CAAA;AACtB,CAAC;AAED,SAAgB,iBAAiB;IAC/B,OAAO,cAAc,CAAA;AACvB,CAAC;AAED,SAAgB,aAAa,CAC3B,IAA6B,EAC7B,IAAY,EACZ,IAA8B;IAE9B,MAAM,KAAK,GAAoB,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IAC1E,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAEvB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,CAAA;IACtC,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,SAAS,GAAG,MAAM,CAAC,CAAA;IAE/D,IAAI,WAAW,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACzC,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,GAAG,eAAe,CAAC,CAAA;IACvE,CAAC;AACH,CAAC;AAED,SAAgB,kBAAkB,CAAC,KAAa;IAC9C,OAAO,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAA;AACnE,CAAC;AAED,SAAgB,gBAAgB;IAC9B,WAAW,GAAG,EAAE,CAAA;IAChB,aAAa,GAAG,SAAS,CAAA;IACzB,cAAc,GAAG,SAAS,CAAA;AAC5B,CAAC;AAEY,QAAA,EAAE,GAAG;IAChB,MAAM,EAAE,CAAC,IAAY,EAAE,IAA8B,EAAE,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC;IAC7F,KAAK,EAAG,CAAC,IAAY,EAAE,IAA8B,EAAE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;IAC5F,GAAG,EAAK,CAAC,MAAc,EAAE,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC;IACpD,KAAK,EAAG,CAAC,IAAY,EAAE,IAA8B,EAAE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;CAC7F,CAAA"}
@@ -0,0 +1,5 @@
1
+ export declare function setupGlobalErrorHandler(): void;
2
+ export declare function handleReactError(error: Error, errorInfo: {
3
+ componentStack: string;
4
+ }): void;
5
+ //# sourceMappingURL=errorHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/client/errorHandler.ts"],"names":[],"mappings":"AAEA,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GACpC,IAAI,CAMN"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setupGlobalErrorHandler = setupGlobalErrorHandler;
4
+ exports.handleReactError = handleReactError;
5
+ const logger_1 = require("./logger");
6
+ function setupGlobalErrorHandler() {
7
+ window.addEventListener('error', (event) => {
8
+ console.error('[fsl] Uncaught error:', event.error);
9
+ (0, logger_1.getClientLogger)().error(event.error, { errorCategory: 'crash', errorType: 'UncaughtError' });
10
+ });
11
+ window.addEventListener('unhandledrejection', (event) => {
12
+ console.error('[fsl] Unhandled rejection:', event.reason);
13
+ (0, logger_1.getClientLogger)().error(event.reason, { errorCategory: 'crash', errorType: 'UnhandledRejection' });
14
+ });
15
+ }
16
+ function handleReactError(error, errorInfo) {
17
+ (0, logger_1.getClientLogger)().error(error, { errorCategory: 'crash', errorType: 'ReactError' }, { componentStack: errorInfo.componentStack });
18
+ }
19
+ //# sourceMappingURL=errorHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/client/errorHandler.ts"],"names":[],"mappings":";;AAEA,0DAUC;AAED,4CASC;AAvBD,qCAA0C;AAE1C,SAAgB,uBAAuB;IACrC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;QACzC,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;QACnD,IAAA,wBAAe,GAAE,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,eAAe,EAAW,CAAC,CAAA;IACvG,CAAC,CAAC,CAAA;IAEF,MAAM,CAAC,gBAAgB,CAAC,oBAAoB,EAAE,CAAC,KAAK,EAAE,EAAE;QACtD,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;QACzD,IAAA,wBAAe,GAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAW,CAAC,CAAA;IAC7G,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAgB,gBAAgB,CAC9B,KAAY,EACZ,SAAqC;IAErC,IAAA,wBAAe,GAAE,CAAC,KAAK,CACrB,KAAK,EACL,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAW,EAC5D,EAAE,cAAc,EAAE,SAAS,CAAC,cAAc,EAAE,CAC7C,CAAA;AACH,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { initLogger, getClientLogger, Logger, triggerTestLog } from './logger';
2
+ export type { InitLoggerConfig, RateLimitConfig } from './logger';
3
+ export { setupGlobalErrorHandler, handleReactError } from './errorHandler';
4
+ export { addBreadcrumb, setActiveActivity, bc } from './breadcrumbs';
5
+ export type { LogSeverity, LogPayload, BreadcrumbEntry, BaseLabels } from '../shared/types';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAC9E,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACjE,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAC1E,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AACpE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bc = exports.setActiveActivity = exports.addBreadcrumb = exports.handleReactError = exports.setupGlobalErrorHandler = exports.triggerTestLog = exports.Logger = exports.getClientLogger = exports.initLogger = void 0;
4
+ var logger_1 = require("./logger");
5
+ Object.defineProperty(exports, "initLogger", { enumerable: true, get: function () { return logger_1.initLogger; } });
6
+ Object.defineProperty(exports, "getClientLogger", { enumerable: true, get: function () { return logger_1.getClientLogger; } });
7
+ Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logger_1.Logger; } });
8
+ Object.defineProperty(exports, "triggerTestLog", { enumerable: true, get: function () { return logger_1.triggerTestLog; } });
9
+ var errorHandler_1 = require("./errorHandler");
10
+ Object.defineProperty(exports, "setupGlobalErrorHandler", { enumerable: true, get: function () { return errorHandler_1.setupGlobalErrorHandler; } });
11
+ Object.defineProperty(exports, "handleReactError", { enumerable: true, get: function () { return errorHandler_1.handleReactError; } });
12
+ var breadcrumbs_1 = require("./breadcrumbs");
13
+ Object.defineProperty(exports, "addBreadcrumb", { enumerable: true, get: function () { return breadcrumbs_1.addBreadcrumb; } });
14
+ Object.defineProperty(exports, "setActiveActivity", { enumerable: true, get: function () { return breadcrumbs_1.setActiveActivity; } });
15
+ Object.defineProperty(exports, "bc", { enumerable: true, get: function () { return breadcrumbs_1.bc; } });
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":";;;AAAA,mCAA8E;AAArE,oGAAA,UAAU,OAAA;AAAE,yGAAA,eAAe,OAAA;AAAE,gGAAA,MAAM,OAAA;AAAE,wGAAA,cAAc,OAAA;AAE5D,+CAA0E;AAAjE,uHAAA,uBAAuB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAClD,6CAAoE;AAA3D,4GAAA,aAAa,OAAA;AAAE,gHAAA,iBAAiB,OAAA;AAAE,iGAAA,EAAE,OAAA"}
@@ -0,0 +1,40 @@
1
+ import type { LogSeverity, LogPayload } from '../shared/types';
2
+ import { type RateLimitConfig } from './rateLimiter';
3
+ export type { RateLimitConfig };
4
+ type LogCallable = (data: LogPayload) => Promise<unknown>;
5
+ export interface InitLoggerConfig<AppLabels extends Record<string, string | undefined> = Record<string, string | undefined>> {
6
+ appId: string;
7
+ releaseId: string;
8
+ logFunction: LogCallable;
9
+ minLogLevel?: LogSeverity;
10
+ rateLimitOptions?: RateLimitConfig;
11
+ }
12
+ export declare class Logger<AppLabels extends Record<string, string | undefined> = Record<string, string | undefined>> {
13
+ private readonly config;
14
+ private readonly minLevel;
15
+ private userId;
16
+ private userLabels;
17
+ constructor(config: InitLoggerConfig<AppLabels>);
18
+ setUser(uid: string, extraLabels?: Partial<AppLabels>): void;
19
+ clearUser(): void;
20
+ setScreen(screen: string): void;
21
+ addBreadcrumb(type: 'action' | 'state' | 'nav' | 'error', name: string, data?: Record<string, unknown>): void;
22
+ error(raw: unknown, labels?: Partial<AppLabels>, context?: Record<string, unknown>, attachments?: Record<string, Blob | File | string>): void;
23
+ info(message: string, labels?: Partial<AppLabels>, context?: Record<string, unknown>, attachments?: Record<string, Blob | File | string>): void;
24
+ warning(message: string, labels?: Partial<AppLabels>, context?: Record<string, unknown>, attachments?: Record<string, Blob | File | string>): void;
25
+ debug(message: string, labels?: Partial<AppLabels>, context?: Record<string, unknown>, attachments?: Record<string, Blob | File | string>): void;
26
+ private send;
27
+ }
28
+ /**
29
+ * Trigger a test log entry to verify the logging pipeline is working end-to-end.
30
+ * Logs at all severities with errorType: 'fsl-verify'. Safe to call in dev only.
31
+ *
32
+ * After clicking, check:
33
+ * 1. dev.jsonl has entries with labels.errorType === 'fsl-verify'
34
+ * 2. Stack trace is symbolicated (points to source file, not minified bundle)
35
+ * 3. MCP query: source: local, where: [{ field: "labels.errorType", operator: "==", value: "fsl-verify" }]
36
+ */
37
+ export declare function triggerTestLog(): void;
38
+ export declare function initLogger<AppLabels extends Record<string, string | undefined> = Record<string, string | undefined>>(config: InitLoggerConfig<AppLabels>): Logger<AppLabels>;
39
+ export declare function getClientLogger<AppLabels extends Record<string, string | undefined> = Record<string, string | undefined>>(): Logger<AppLabels>;
40
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/client/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAS9D,OAAO,EAML,KAAK,eAAe,EACrB,MAAM,eAAe,CAAA;AAEtB,YAAY,EAAE,eAAe,EAAE,CAAA;AAE/B,KAAK,WAAW,GAAG,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAEzD,MAAM,WAAW,gBAAgB,CAC/B,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzF,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,WAAW,CAAA;IACxB,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,gBAAgB,CAAC,EAAE,eAAe,CAAA;CACnC;AAoDD,qBAAa,MAAM,CACjB,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAyB;gBAE/B,MAAM,EAAE,gBAAgB,CAAC,SAAS,CAAC;IAQ/C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI;IAK5D,SAAS,IAAI,IAAI;IAMjB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,aAAa,CACX,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,EAC1C,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI;IAIP,KAAK,CACH,GAAG,EAAE,OAAO,EACZ,MAAM,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,EAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC,GACjD,IAAI;IAyBP,IAAI,CACF,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,EAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC,GACjD,IAAI;IAIP,OAAO,CACL,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,EAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC,GACjD,IAAI;IAIP,KAAK,CACH,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,EAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC,GACjD,IAAI;YAIO,IAAI;CAsDnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAWrC;AAKD,wBAAgB,UAAU,CACxB,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EACzF,MAAM,EAAE,gBAAgB,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAGxD;AAED,wBAAgB,eAAe,CAC7B,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KACtF,MAAM,CAAC,SAAS,CAAC,CAGrB"}
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Logger = void 0;
4
+ exports.triggerTestLog = triggerTestLog;
5
+ exports.initLogger = initLogger;
6
+ exports.getClientLogger = getClientLogger;
7
+ const breadcrumbs_1 = require("./breadcrumbs");
8
+ const rateLimiter_1 = require("./rateLimiter");
9
+ const SEVERITY_ORDER = {
10
+ ERROR: 0,
11
+ WARNING: 1,
12
+ INFO: 2,
13
+ DEBUG: 3,
14
+ };
15
+ function defaultMinLevel() {
16
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production')
17
+ return 'WARNING';
18
+ return 'DEBUG';
19
+ }
20
+ function getPlatform() {
21
+ if (typeof navigator === 'undefined')
22
+ return 'unknown';
23
+ const ua = navigator.userAgent;
24
+ if (/iPhone|iPad|iPod/.test(ua))
25
+ return 'ios';
26
+ if (/Android/.test(ua))
27
+ return 'android';
28
+ if (/Mac/.test(ua))
29
+ return 'macos';
30
+ if (/Win/.test(ua))
31
+ return 'windows';
32
+ if (/Linux/.test(ua))
33
+ return 'linux';
34
+ return 'web';
35
+ }
36
+ function getBrowser() {
37
+ if (typeof navigator === 'undefined')
38
+ return 'unknown';
39
+ const ua = navigator.userAgent;
40
+ if (/Firefox/.test(ua))
41
+ return 'firefox';
42
+ if (/Edg/.test(ua))
43
+ return 'edge';
44
+ if (/Chrome/.test(ua))
45
+ return 'chrome';
46
+ if (/Safari/.test(ua))
47
+ return 'safari';
48
+ return 'unknown';
49
+ }
50
+ async function blobToBase64(blob) {
51
+ return new Promise((resolve, reject) => {
52
+ const reader = new FileReader();
53
+ reader.onload = () => {
54
+ const result = reader.result;
55
+ resolve(result.split(',')[1] ?? result);
56
+ };
57
+ reader.onerror = reject;
58
+ reader.readAsDataURL(blob);
59
+ });
60
+ }
61
+ async function assetToBase64(asset) {
62
+ if (typeof asset === 'string')
63
+ return asset;
64
+ return blobToBase64(asset);
65
+ }
66
+ class Logger {
67
+ config;
68
+ minLevel;
69
+ userId;
70
+ userLabels = {};
71
+ constructor(config) {
72
+ this.config = config;
73
+ this.minLevel = SEVERITY_ORDER[config.minLogLevel ?? defaultMinLevel()];
74
+ if (config.rateLimitOptions) {
75
+ (0, rateLimiter_1.configureRateLimiter)(config.rateLimitOptions);
76
+ }
77
+ }
78
+ setUser(uid, extraLabels) {
79
+ this.userId = uid;
80
+ this.userLabels = extraLabels ?? {};
81
+ }
82
+ clearUser() {
83
+ this.userId = undefined;
84
+ this.userLabels = {};
85
+ (0, breadcrumbs_1.clearBreadcrumbs)();
86
+ }
87
+ setScreen(screen) {
88
+ (0, breadcrumbs_1.setCurrentScreen)(screen);
89
+ }
90
+ addBreadcrumb(type, name, data) {
91
+ (0, breadcrumbs_1.addBreadcrumb)(type, name, data);
92
+ }
93
+ error(raw, labels, context, attachments) {
94
+ const error = raw instanceof Error ? raw : new Error(String(raw));
95
+ const screen = (0, breadcrumbs_1.getCurrentScreen)();
96
+ const activity = (0, breadcrumbs_1.getActiveActivity)();
97
+ if (!(0, rateLimiter_1.canLogError)(error, screen, activity)) {
98
+ console.error('[fsl] Error suppressed:', error.message);
99
+ return;
100
+ }
101
+ (0, rateLimiter_1.recordError)(error, screen, activity);
102
+ const errorLabels = {
103
+ errorType: error.name || 'UnknownError',
104
+ ...labels,
105
+ };
106
+ void this.send(error.message, 'ERROR', errorLabels, context, attachments, {
107
+ message: error.message,
108
+ stack: error.stack,
109
+ name: error.name,
110
+ cause: error.cause != null ? String(error.cause) : undefined,
111
+ });
112
+ }
113
+ info(message, labels, context, attachments) {
114
+ void this.send(message, 'INFO', labels, context, attachments);
115
+ }
116
+ warning(message, labels, context, attachments) {
117
+ void this.send(message, 'WARNING', labels, context, attachments);
118
+ }
119
+ debug(message, labels, context, attachments) {
120
+ void this.send(message, 'DEBUG', labels, context, attachments);
121
+ }
122
+ async send(message, severity, labels, context, attachments, error) {
123
+ if (SEVERITY_ORDER[severity] > this.minLevel)
124
+ return;
125
+ if (!(0, rateLimiter_1.canLogEvent)()) {
126
+ console.warn('[fsl] Rate limit exceeded');
127
+ return;
128
+ }
129
+ try {
130
+ const allLabels = {
131
+ appId: this.config.appId,
132
+ releaseId: this.config.releaseId,
133
+ screen: (0, breadcrumbs_1.getCurrentScreen)(),
134
+ userId: this.userId,
135
+ platform: getPlatform(),
136
+ browser: getBrowser(),
137
+ ...this.userLabels,
138
+ ...labels,
139
+ };
140
+ let base64Attachments;
141
+ if (attachments && Object.keys(attachments).length > 0) {
142
+ base64Attachments = {};
143
+ for (const [name, attachment] of Object.entries(attachments)) {
144
+ base64Attachments[name] = await assetToBase64(attachment);
145
+ }
146
+ }
147
+ const payload = {
148
+ message,
149
+ severity,
150
+ labels: allLabels,
151
+ jsonPayload: {
152
+ breadcrumbs: (0, breadcrumbs_1.getLastBreadcrumbs)(20),
153
+ context,
154
+ error,
155
+ },
156
+ ...(base64Attachments ? { attachments: base64Attachments } : {}),
157
+ };
158
+ (0, rateLimiter_1.recordLog)();
159
+ await this.config.logFunction(payload);
160
+ }
161
+ catch (err) {
162
+ console.error('[fsl] Failed to send log:', err instanceof Error ? err.message : err);
163
+ }
164
+ }
165
+ }
166
+ exports.Logger = Logger;
167
+ /**
168
+ * Trigger a test log entry to verify the logging pipeline is working end-to-end.
169
+ * Logs at all severities with errorType: 'fsl-verify'. Safe to call in dev only.
170
+ *
171
+ * After clicking, check:
172
+ * 1. dev.jsonl has entries with labels.errorType === 'fsl-verify'
173
+ * 2. Stack trace is symbolicated (points to source file, not minified bundle)
174
+ * 3. MCP query: source: local, where: [{ field: "labels.errorType", operator: "==", value: "fsl-verify" }]
175
+ */
176
+ function triggerTestLog() {
177
+ console.info('[fsl] triggerTestLog called');
178
+ const logger = getClientLogger();
179
+ const testError = new Error('[fsl-verify] Test error — logging pipeline check');
180
+ console.info('[fsl] sending error log...');
181
+ logger.error(testError, { errorType: 'fsl-verify' }, { test: true });
182
+ console.info('[fsl] sending warning log...');
183
+ logger.warning('[fsl-verify] Test warning', { errorType: 'fsl-verify' });
184
+ console.info('[fsl] sending info log...');
185
+ logger.info('[fsl-verify] Test info', { errorType: 'fsl-verify' });
186
+ console.info('[fsl] triggerTestLog scheduled — sends are fire-and-forget; check dev.jsonl in ~1-2s');
187
+ }
188
+ // Module-level singleton
189
+ let instance = null;
190
+ function initLogger(config) {
191
+ instance = new Logger(config);
192
+ return instance;
193
+ }
194
+ function getClientLogger() {
195
+ if (!instance)
196
+ throw new Error('[fsl] initLogger() not called');
197
+ return instance;
198
+ }
199
+ //# sourceMappingURL=logger.js.map