@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.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/client/breadcrumbs.d.ts +15 -0
- package/dist/client/breadcrumbs.d.ts.map +1 -0
- package/dist/client/breadcrumbs.js +52 -0
- package/dist/client/breadcrumbs.js.map +1 -0
- package/dist/client/errorHandler.d.ts +5 -0
- package/dist/client/errorHandler.d.ts.map +1 -0
- package/dist/client/errorHandler.js +19 -0
- package/dist/client/errorHandler.js.map +1 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +16 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/logger.d.ts +40 -0
- package/dist/client/logger.d.ts.map +1 -0
- package/dist/client/logger.js +199 -0
- package/dist/client/logger.js.map +1 -0
- package/dist/client/rateLimiter.d.ts +12 -0
- package/dist/client/rateLimiter.d.ts.map +1 -0
- package/dist/client/rateLimiter.js +80 -0
- package/dist/client/rateLimiter.js.map +1 -0
- package/dist/functions/index.d.ts +12 -0
- package/dist/functions/index.d.ts.map +1 -0
- package/dist/functions/index.js +21 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/functions/logHandler.d.ts +27 -0
- package/dist/functions/logHandler.d.ts.map +1 -0
- package/dist/functions/logHandler.js +137 -0
- package/dist/functions/logHandler.js.map +1 -0
- package/dist/functions/logger.d.ts +32 -0
- package/dist/functions/logger.d.ts.map +1 -0
- package/dist/functions/logger.js +253 -0
- package/dist/functions/logger.js.map +1 -0
- package/dist/functions/requestLogger.d.ts +30 -0
- package/dist/functions/requestLogger.d.ts.map +1 -0
- package/dist/functions/requestLogger.js +45 -0
- package/dist/functions/requestLogger.js.map +1 -0
- package/dist/functions/sourceMapCache.d.ts +9 -0
- package/dist/functions/sourceMapCache.d.ts.map +1 -0
- package/dist/functions/sourceMapCache.js +107 -0
- package/dist/functions/sourceMapCache.js.map +1 -0
- package/dist/functions/symbolicate.d.ts +31 -0
- package/dist/functions/symbolicate.d.ts.map +1 -0
- package/dist/functions/symbolicate.js +112 -0
- package/dist/functions/symbolicate.js.map +1 -0
- package/dist/shared/types.d.ts +36 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +3 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +152 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/installSkills.d.ts +6 -0
- package/dist/tools/installSkills.d.ts.map +1 -0
- package/dist/tools/installSkills.js +89 -0
- package/dist/tools/installSkills.js.map +1 -0
- package/dist/tools/packDeploy.d.ts +15 -0
- package/dist/tools/packDeploy.d.ts.map +1 -0
- package/dist/tools/packDeploy.js +148 -0
- package/dist/tools/packDeploy.js.map +1 -0
- package/dist/tools/uploadSourceMaps.d.ts +9 -0
- package/dist/tools/uploadSourceMaps.d.ts.map +1 -0
- package/dist/tools/uploadSourceMaps.js +120 -0
- package/dist/tools/uploadSourceMaps.js.map +1 -0
- package/package.json +103 -0
- package/skills/logs/SKILL.md +185 -0
- 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 @@
|
|
|
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":"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"}
|
|
@@ -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
|