@ga-components/logger 1.0.8 → 1.0.9
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/dist/index.d.mts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +148 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +147 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Component, ReactNode, ErrorInfo } from 'react';
|
|
3
|
+
|
|
1
4
|
type LogLevel = "info" | "warn" | "error";
|
|
2
5
|
type LogEnv = "PROD" | "DEV";
|
|
6
|
+
type BreadcrumbType = "navigation" | "click" | "fetch" | "auth" | "error" | "manual";
|
|
7
|
+
interface Breadcrumb {
|
|
8
|
+
timestamp: Date;
|
|
9
|
+
type: BreadcrumbType;
|
|
10
|
+
message: string;
|
|
11
|
+
data?: Record<string, unknown>;
|
|
12
|
+
}
|
|
3
13
|
interface LogEntry {
|
|
4
14
|
timestamp: Date;
|
|
5
15
|
level: LogLevel;
|
|
@@ -12,6 +22,7 @@ interface LogEntry {
|
|
|
12
22
|
payload: Record<string, unknown>;
|
|
13
23
|
repo: string;
|
|
14
24
|
endpoint: string | null;
|
|
25
|
+
breadcrumbs: Breadcrumb[];
|
|
15
26
|
}
|
|
16
27
|
interface LoggerConfig {
|
|
17
28
|
env: LogEnv;
|
|
@@ -34,8 +45,13 @@ type LogParams = {
|
|
|
34
45
|
declare class Logger {
|
|
35
46
|
private config;
|
|
36
47
|
private initialized;
|
|
48
|
+
private breadcrumbs;
|
|
37
49
|
init(config: LoggerConfig): void;
|
|
38
50
|
setUserId(userId: string | null): void;
|
|
51
|
+
addBreadcrumb(type: BreadcrumbType, message: string, data?: Record<string, unknown>): void;
|
|
52
|
+
initGlobalCapture(): void;
|
|
53
|
+
private shouldSkipGlobalError;
|
|
54
|
+
interceptFetch(): void;
|
|
39
55
|
captureAction<T>(params: LogParams, fn: () => T | Promise<T>): Promise<T | undefined>;
|
|
40
56
|
private send;
|
|
41
57
|
info(params: LogParams): Promise<void>;
|
|
@@ -44,4 +60,18 @@ declare class Logger {
|
|
|
44
60
|
}
|
|
45
61
|
declare const logger: Logger;
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
interface Props {
|
|
64
|
+
children: ReactNode;
|
|
65
|
+
fallback?: ReactNode;
|
|
66
|
+
}
|
|
67
|
+
interface State {
|
|
68
|
+
hasError: boolean;
|
|
69
|
+
}
|
|
70
|
+
declare class LoggerErrorBoundary extends Component<Props, State> {
|
|
71
|
+
constructor(props: Props);
|
|
72
|
+
static getDerivedStateFromError(): State;
|
|
73
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
74
|
+
render(): string | number | boolean | Iterable<ReactNode> | react.JSX.Element | null | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { type Breadcrumb, type BreadcrumbType, type LogEntry, type LogEnv, type LogLevel, type LoggerConfig, LoggerErrorBoundary, logger };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Component, ReactNode, ErrorInfo } from 'react';
|
|
3
|
+
|
|
1
4
|
type LogLevel = "info" | "warn" | "error";
|
|
2
5
|
type LogEnv = "PROD" | "DEV";
|
|
6
|
+
type BreadcrumbType = "navigation" | "click" | "fetch" | "auth" | "error" | "manual";
|
|
7
|
+
interface Breadcrumb {
|
|
8
|
+
timestamp: Date;
|
|
9
|
+
type: BreadcrumbType;
|
|
10
|
+
message: string;
|
|
11
|
+
data?: Record<string, unknown>;
|
|
12
|
+
}
|
|
3
13
|
interface LogEntry {
|
|
4
14
|
timestamp: Date;
|
|
5
15
|
level: LogLevel;
|
|
@@ -12,6 +22,7 @@ interface LogEntry {
|
|
|
12
22
|
payload: Record<string, unknown>;
|
|
13
23
|
repo: string;
|
|
14
24
|
endpoint: string | null;
|
|
25
|
+
breadcrumbs: Breadcrumb[];
|
|
15
26
|
}
|
|
16
27
|
interface LoggerConfig {
|
|
17
28
|
env: LogEnv;
|
|
@@ -34,8 +45,13 @@ type LogParams = {
|
|
|
34
45
|
declare class Logger {
|
|
35
46
|
private config;
|
|
36
47
|
private initialized;
|
|
48
|
+
private breadcrumbs;
|
|
37
49
|
init(config: LoggerConfig): void;
|
|
38
50
|
setUserId(userId: string | null): void;
|
|
51
|
+
addBreadcrumb(type: BreadcrumbType, message: string, data?: Record<string, unknown>): void;
|
|
52
|
+
initGlobalCapture(): void;
|
|
53
|
+
private shouldSkipGlobalError;
|
|
54
|
+
interceptFetch(): void;
|
|
39
55
|
captureAction<T>(params: LogParams, fn: () => T | Promise<T>): Promise<T | undefined>;
|
|
40
56
|
private send;
|
|
41
57
|
info(params: LogParams): Promise<void>;
|
|
@@ -44,4 +60,18 @@ declare class Logger {
|
|
|
44
60
|
}
|
|
45
61
|
declare const logger: Logger;
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
interface Props {
|
|
64
|
+
children: ReactNode;
|
|
65
|
+
fallback?: ReactNode;
|
|
66
|
+
}
|
|
67
|
+
interface State {
|
|
68
|
+
hasError: boolean;
|
|
69
|
+
}
|
|
70
|
+
declare class LoggerErrorBoundary extends Component<Props, State> {
|
|
71
|
+
constructor(props: Props);
|
|
72
|
+
static getDerivedStateFromError(): State;
|
|
73
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
74
|
+
render(): string | number | boolean | Iterable<ReactNode> | react.JSX.Element | null | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { type Breadcrumb, type BreadcrumbType, type LogEntry, type LogEnv, type LogLevel, type LoggerConfig, LoggerErrorBoundary, logger };
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
LoggerErrorBoundary: () => LoggerErrorBoundary,
|
|
23
24
|
logger: () => logger
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -55,10 +56,12 @@ var getDb = () => db;
|
|
|
55
56
|
|
|
56
57
|
// src/core/logger.ts
|
|
57
58
|
var sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
59
|
+
var MAX_BREADCRUMBS = 50;
|
|
58
60
|
var Logger = class {
|
|
59
61
|
constructor() {
|
|
60
62
|
this.config = null;
|
|
61
63
|
this.initialized = false;
|
|
64
|
+
this.breadcrumbs = [];
|
|
62
65
|
}
|
|
63
66
|
init(config) {
|
|
64
67
|
this.config = config;
|
|
@@ -73,6 +76,112 @@ var Logger = class {
|
|
|
73
76
|
}
|
|
74
77
|
this.config.userId = userId;
|
|
75
78
|
}
|
|
79
|
+
// ✅ NEW: Add breadcrumb ke in-memory array
|
|
80
|
+
addBreadcrumb(type, message, data) {
|
|
81
|
+
const breadcrumb = {
|
|
82
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
83
|
+
type,
|
|
84
|
+
message,
|
|
85
|
+
data
|
|
86
|
+
};
|
|
87
|
+
this.breadcrumbs.push(breadcrumb);
|
|
88
|
+
if (this.breadcrumbs.length > MAX_BREADCRUMBS) {
|
|
89
|
+
this.breadcrumbs.shift();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ✅ NEW: Global error capture — hanya aktif di non-development
|
|
93
|
+
initGlobalCapture() {
|
|
94
|
+
if (!this.config || this.config.env === "DEV") {
|
|
95
|
+
console.info("[ga-logger] global capture skipped \u2014 DEV environment");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
window.onerror = (message, source, lineno, colno, error) => {
|
|
99
|
+
if (this.shouldSkipGlobalError(message?.toString(), source)) return false;
|
|
100
|
+
this.addBreadcrumb("error", `window.onerror: ${message}`, {
|
|
101
|
+
source,
|
|
102
|
+
lineno,
|
|
103
|
+
colno
|
|
104
|
+
});
|
|
105
|
+
this.error({
|
|
106
|
+
fnName: "window.onerror",
|
|
107
|
+
messageError: error ?? new Error(String(message)),
|
|
108
|
+
payload: { source, lineno, colno }
|
|
109
|
+
});
|
|
110
|
+
return false;
|
|
111
|
+
};
|
|
112
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
113
|
+
const message = event.reason?.message ?? String(event.reason);
|
|
114
|
+
if (this.shouldSkipGlobalError(message)) return;
|
|
115
|
+
const error = event.reason instanceof Error ? event.reason : new Error(message);
|
|
116
|
+
this.addBreadcrumb("error", `unhandledrejection: ${message}`);
|
|
117
|
+
this.error({
|
|
118
|
+
fnName: "window.unhandledrejection",
|
|
119
|
+
messageError: error
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
console.info("[ga-logger] global capture active");
|
|
123
|
+
}
|
|
124
|
+
// ✅ NEW: Smart filter — skip noise dari CORS, network, third-party
|
|
125
|
+
shouldSkipGlobalError(message, source) {
|
|
126
|
+
if (!message) return true;
|
|
127
|
+
const noisePatterns = [
|
|
128
|
+
"CORS",
|
|
129
|
+
"cross-origin",
|
|
130
|
+
"ERR_FAILED",
|
|
131
|
+
"Failed to load resource",
|
|
132
|
+
"NetworkError",
|
|
133
|
+
"ResizeObserver loop",
|
|
134
|
+
"Script error"
|
|
135
|
+
];
|
|
136
|
+
const isNoise = noisePatterns.some((pattern) => message.toLowerCase().includes(pattern.toLowerCase()));
|
|
137
|
+
if (isNoise) return true;
|
|
138
|
+
if (source) {
|
|
139
|
+
const thirdPartyPatterns = ["magnolia", "gtm.js", "analytics", "googletagmanager", "facebook", "hotjar"];
|
|
140
|
+
const isThirdParty = thirdPartyPatterns.some((pattern) => source.includes(pattern));
|
|
141
|
+
if (isThirdParty) return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
// ✅ NEW: interceptFetch — auto breadcrumb, skip 4xx/5xx ke Firestore
|
|
146
|
+
interceptFetch() {
|
|
147
|
+
if (typeof window === "undefined") return;
|
|
148
|
+
const originalFetch = window.fetch;
|
|
149
|
+
window.fetch = async (input, init) => {
|
|
150
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
151
|
+
const method = init?.method ?? "GET";
|
|
152
|
+
const startTime = Date.now();
|
|
153
|
+
try {
|
|
154
|
+
const response = await originalFetch(input, init);
|
|
155
|
+
const duration = Date.now() - startTime;
|
|
156
|
+
const status = response.status;
|
|
157
|
+
this.addBreadcrumb("fetch", `${method} ${url}`, {
|
|
158
|
+
status,
|
|
159
|
+
duration: `${duration}ms`
|
|
160
|
+
});
|
|
161
|
+
if (status >= 400) {
|
|
162
|
+
console.warn(`[ga-logger] HTTP ${status} skipped \u2014 Kibana handles this`, url);
|
|
163
|
+
}
|
|
164
|
+
return response;
|
|
165
|
+
} catch (networkError) {
|
|
166
|
+
const duration = Date.now() - startTime;
|
|
167
|
+
const error = networkError instanceof Error ? networkError : new Error(String(networkError));
|
|
168
|
+
this.addBreadcrumb("fetch", `${method} ${url} \u2014 NETWORK ERROR`, {
|
|
169
|
+
duration: `${duration}ms`,
|
|
170
|
+
error: error.message
|
|
171
|
+
});
|
|
172
|
+
if (this.config?.env !== "DEV") {
|
|
173
|
+
this.error({
|
|
174
|
+
fnName: "interceptFetch",
|
|
175
|
+
messageError: error,
|
|
176
|
+
endpointName: url,
|
|
177
|
+
payload: { method, duration: `${duration}ms` }
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
throw networkError;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
console.info("[ga-logger] fetch intercepted");
|
|
184
|
+
}
|
|
76
185
|
captureAction(params, fn) {
|
|
77
186
|
return Promise.resolve().then(() => fn()).catch((error) => {
|
|
78
187
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -85,6 +194,10 @@ var Logger = class {
|
|
|
85
194
|
console.warn("[ga-logger] not initialized. Call logger.init() first.");
|
|
86
195
|
return;
|
|
87
196
|
}
|
|
197
|
+
if (this.config.env === "DEV") {
|
|
198
|
+
console.error(`[ga-logger][DEV] ${message}`, { payload, error });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
88
201
|
const entry = {
|
|
89
202
|
timestamp: /* @__PURE__ */ new Date(),
|
|
90
203
|
level,
|
|
@@ -96,7 +209,9 @@ var Logger = class {
|
|
|
96
209
|
stackTrace: error?.stack ?? null,
|
|
97
210
|
payload,
|
|
98
211
|
repo: this.config.repo,
|
|
99
|
-
endpoint: endpoint ?? null
|
|
212
|
+
endpoint: endpoint ?? null,
|
|
213
|
+
breadcrumbs: [...this.breadcrumbs]
|
|
214
|
+
// ← snapshot saat error terjadi
|
|
100
215
|
};
|
|
101
216
|
try {
|
|
102
217
|
const db2 = getDb();
|
|
@@ -122,8 +237,40 @@ var Logger = class {
|
|
|
122
237
|
}
|
|
123
238
|
};
|
|
124
239
|
var logger = new Logger();
|
|
240
|
+
|
|
241
|
+
// src/components/LoggerErrorBoundary.tsx
|
|
242
|
+
var import_react = require("react");
|
|
243
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
244
|
+
var LoggerErrorBoundary = class extends import_react.Component {
|
|
245
|
+
constructor(props) {
|
|
246
|
+
super(props);
|
|
247
|
+
this.state = { hasError: false };
|
|
248
|
+
}
|
|
249
|
+
static getDerivedStateFromError() {
|
|
250
|
+
return { hasError: true };
|
|
251
|
+
}
|
|
252
|
+
componentDidCatch(error, info) {
|
|
253
|
+
logger.addBreadcrumb("error", `React crash: ${error.message}`, {
|
|
254
|
+
componentStack: info.componentStack ?? void 0
|
|
255
|
+
});
|
|
256
|
+
logger.error({
|
|
257
|
+
fnName: "LoggerErrorBoundary",
|
|
258
|
+
messageError: error,
|
|
259
|
+
payload: {
|
|
260
|
+
componentStack: info.componentStack ?? void 0
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
render() {
|
|
265
|
+
if (this.state.hasError) {
|
|
266
|
+
return this.props.fallback ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: "1rem", color: "red" }, children: "Something went wrong." });
|
|
267
|
+
}
|
|
268
|
+
return this.props.children;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
125
271
|
// Annotate the CommonJS export names for ESM import in node:
|
|
126
272
|
0 && (module.exports = {
|
|
273
|
+
LoggerErrorBoundary,
|
|
127
274
|
logger
|
|
128
275
|
});
|
|
129
276
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/core/logger.ts","../src/config/firebase.ts"],"sourcesContent":["export { logger } from \"./core/logger\";\nexport type { LogEntry, LoggerConfig, LogLevel, LogEnv } from \"./types\";\n","import { collection, addDoc, Timestamp } from \"firebase/firestore\";\nimport { initFirebase, getDb } from \"../config/firebase\";\nimport type { LogEntry, LogLevel, LogParams, LoggerConfig } from \"../types\";\n\nconst sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n\nclass Logger {\n private config: LoggerConfig | null = null;\n private initialized = false;\n\n init(config: LoggerConfig) {\n this.config = config;\n initFirebase(config);\n this.initialized = true;\n console.info(\"[ga-logger] initialized —\", config.env);\n }\n\n setUserId(userId: string | null) {\n if (!this.config) {\n console.warn(\"[ga-logger] not initialized. Call logger.init() first.\");\n return;\n }\n this.config.userId = userId;\n }\n \n captureAction<T>(params: LogParams, fn: () => T | Promise<T>): Promise<T | undefined> {\n return Promise.resolve()\n .then(() => fn())\n .catch((error: unknown) => {\n const err = error instanceof Error ? error : new Error(String(error));\n this.send(\"error\", params.fnName, params.payload, err, params.endpointName);\n return undefined;\n });\n }\n private async send(\n level: LogLevel,\n message: string,\n payload: Record<string, unknown> = {},\n error?: Error,\n endpoint?: string,\n ) {\n if (!this.initialized || !this.config) {\n console.warn(\"[ga-logger] not initialized. Call logger.init() first.\");\n return;\n }\n\n const entry: LogEntry = {\n timestamp: new Date(),\n level,\n message,\n env: this.config.env,\n userId: this.config.userId ?? null,\n sessionId,\n page: typeof window !== \"undefined\" ? window.location.pathname : \"\",\n stackTrace: error?.stack ?? null,\n payload,\n repo: this.config.repo,\n endpoint: endpoint ?? null,\n };\n try {\n const db = getDb();\n await addDoc(collection(db, \"logs\"), {\n ...entry,\n timestamp: Timestamp.fromDate(entry.timestamp),\n });\n } catch (e) {\n console.error(\"[ga-logger] failed to send log:\", e);\n }\n }\n\n info(params: LogParams) {\n const { fnName, payload, endpointName } = params;\n return this.send(\"info\", fnName, payload, undefined, endpointName);\n }\n\n warn(params: LogParams) {\n const { fnName, payload, endpointName } = params;\n return this.send(\"warn\", fnName, payload, undefined, endpointName);\n }\n\n error(params: LogParams) {\n const { fnName, messageError, payload, endpointName } = params;\n return this.send(\"error\", fnName, payload, messageError, endpointName);\n }\n}\n\n\n\nexport const logger = new Logger();","import { initializeApp, getApps, FirebaseApp } from \"firebase/app\";\nimport { getFirestore, Firestore } from \"firebase/firestore\";\nimport { LoggerConfig } from \"../types\";\n\nlet app: FirebaseApp;\nlet db: Firestore;\n\nexport const initFirebase = (config: LoggerConfig) => {\n if (!getApps().length) {\n app = initializeApp(\n {\n apiKey: config.apiKey,\n authDomain: config.authDomain,\n projectId: config.projectId,\n storageBucket: config.storageBucket,\n messagingSenderId: config.messagingSenderId,\n appId: config.appId,\n },\n \"ga-logger\",\n );\n } else {\n app = getApps()[0];\n }\n\n db = getFirestore(app);\n return db;\n};\n\nexport const getDb = () => db;\n"],"mappings":"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","names":["import_firestore","db"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/logger.ts","../src/config/firebase.ts","../src/components/LoggerErrorBoundary.tsx"],"sourcesContent":["export { logger } from \"./core/logger\";\nexport { LoggerErrorBoundary } from \"./components/LoggerErrorBoundary\"; // ← baru\nexport type { LogEntry, LoggerConfig, LogLevel, LogEnv, Breadcrumb, BreadcrumbType } from \"./types\";\n","import { collection, addDoc, Timestamp } from \"firebase/firestore\";\nimport { initFirebase, getDb } from \"../config/firebase\";\nimport type { Breadcrumb, BreadcrumbType, LogEntry, LogLevel, LogParams, LoggerConfig } from \"../types\";\n\nconst sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\nconst MAX_BREADCRUMBS = 50;\n\nclass Logger {\n private config: LoggerConfig | null = null;\n private initialized = false;\n private breadcrumbs: Breadcrumb[] = [];\n\n init(config: LoggerConfig) {\n this.config = config;\n initFirebase(config);\n this.initialized = true;\n console.info(\"[ga-logger] initialized —\", config.env);\n }\n\n setUserId(userId: string | null) {\n if (!this.config) {\n console.warn(\"[ga-logger] not initialized. Call logger.init() first.\");\n return;\n }\n this.config.userId = userId;\n }\n\n // ✅ NEW: Add breadcrumb ke in-memory array\n addBreadcrumb(type: BreadcrumbType, message: string, data?: Record<string, unknown>) {\n const breadcrumb: Breadcrumb = {\n timestamp: new Date(),\n type,\n message,\n data,\n };\n\n this.breadcrumbs.push(breadcrumb);\n\n // Jaga memory — max 50 breadcrumbs, buang yang paling lama\n if (this.breadcrumbs.length > MAX_BREADCRUMBS) {\n this.breadcrumbs.shift();\n }\n }\n\n // ✅ NEW: Global error capture — hanya aktif di non-development\n initGlobalCapture() {\n if (!this.config || this.config.env === \"DEV\") {\n console.info(\"[ga-logger] global capture skipped — DEV environment\");\n return;\n }\n\n // Capture JS runtime error\n window.onerror = (message, source, lineno, colno, error) => {\n if (this.shouldSkipGlobalError(message?.toString(), source)) return false;\n\n this.addBreadcrumb(\"error\", `window.onerror: ${message}`, {\n source,\n lineno,\n colno,\n });\n\n this.error({\n fnName: \"window.onerror\",\n messageError: error ?? new Error(String(message)),\n payload: { source, lineno, colno },\n });\n\n return false;\n };\n\n // Capture unhandled Promise rejection\n window.addEventListener(\"unhandledrejection\", (event) => {\n const message = event.reason?.message ?? String(event.reason);\n\n if (this.shouldSkipGlobalError(message)) return;\n\n const error = event.reason instanceof Error ? event.reason : new Error(message);\n\n this.addBreadcrumb(\"error\", `unhandledrejection: ${message}`);\n\n this.error({\n fnName: \"window.unhandledrejection\",\n messageError: error,\n });\n });\n\n console.info(\"[ga-logger] global capture active\");\n }\n\n // ✅ NEW: Smart filter — skip noise dari CORS, network, third-party\n private shouldSkipGlobalError(message?: string, source?: string): boolean {\n if (!message) return true;\n\n const noisePatterns = [\n \"CORS\",\n \"cross-origin\",\n \"ERR_FAILED\",\n \"Failed to load resource\",\n \"NetworkError\",\n \"ResizeObserver loop\",\n \"Script error\",\n ];\n\n const isNoise = noisePatterns.some((pattern) => message.toLowerCase().includes(pattern.toLowerCase()));\n if (isNoise) return true;\n\n // Skip error dari third-party source\n if (source) {\n const thirdPartyPatterns = [\"magnolia\", \"gtm.js\", \"analytics\", \"googletagmanager\", \"facebook\", \"hotjar\"];\n const isThirdParty = thirdPartyPatterns.some((pattern) => source.includes(pattern));\n if (isThirdParty) return true;\n }\n\n return false;\n }\n\n // ✅ NEW: interceptFetch — auto breadcrumb, skip 4xx/5xx ke Firestore\n interceptFetch() {\n if (typeof window === \"undefined\") return;\n\n const originalFetch = window.fetch;\n\n window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n const method = init?.method ?? \"GET\";\n const startTime = Date.now();\n\n try {\n const response = await originalFetch(input, init);\n const duration = Date.now() - startTime;\n const status = response.status;\n\n // Selalu add breadcrumb untuk semua fetch\n this.addBreadcrumb(\"fetch\", `${method} ${url}`, {\n status,\n duration: `${duration}ms`,\n });\n\n // 4xx/5xx → skip logger.error, Kibana sudah handle\n if (status >= 400) {\n console.warn(`[ga-logger] HTTP ${status} skipped — Kibana handles this`, url);\n }\n\n return response;\n } catch (networkError) {\n const duration = Date.now() - startTime;\n const error = networkError instanceof Error ? networkError : new Error(String(networkError));\n\n // Network failure → Kibana tidak tahu, kita log!\n this.addBreadcrumb(\"fetch\", `${method} ${url} — NETWORK ERROR`, {\n duration: `${duration}ms`,\n error: error.message,\n });\n\n // Hanya log jika bukan DEV\n if (this.config?.env !== \"DEV\") {\n this.error({\n fnName: \"interceptFetch\",\n messageError: error,\n endpointName: url,\n payload: { method, duration: `${duration}ms` },\n });\n }\n\n throw networkError;\n }\n };\n\n console.info(\"[ga-logger] fetch intercepted\");\n }\n\n captureAction<T>(params: LogParams, fn: () => T | Promise<T>): Promise<T | undefined> {\n return Promise.resolve()\n .then(() => fn())\n .catch((error: unknown) => {\n const err = error instanceof Error ? error : new Error(String(error));\n this.send(\"error\", params.fnName, params.payload, err, params.endpointName);\n return undefined;\n });\n }\n\n private async send(\n level: LogLevel,\n message: string,\n payload: Record<string, unknown> = {},\n error?: Error,\n endpoint?: string,\n ) {\n if (!this.initialized || !this.config) {\n console.warn(\"[ga-logger] not initialized. Call logger.init() first.\");\n return;\n }\n\n // DEV → console only, skip Firestore\n if (this.config.env === \"DEV\") {\n console.error(`[ga-logger][DEV] ${message}`, { payload, error });\n return;\n }\n\n const entry: LogEntry = {\n timestamp: new Date(),\n level,\n message,\n env: this.config.env,\n userId: this.config.userId ?? null,\n sessionId,\n page: typeof window !== \"undefined\" ? window.location.pathname : \"\",\n stackTrace: error?.stack ?? null,\n payload,\n repo: this.config.repo,\n endpoint: endpoint ?? null,\n breadcrumbs: [...this.breadcrumbs], // ← snapshot saat error terjadi\n };\n\n try {\n const db = getDb();\n await addDoc(collection(db, \"logs\"), {\n ...entry,\n timestamp: Timestamp.fromDate(entry.timestamp),\n });\n } catch (e) {\n console.error(\"[ga-logger] failed to send log:\", e);\n }\n }\n\n info(params: LogParams) {\n const { fnName, payload, endpointName } = params;\n return this.send(\"info\", fnName, payload, undefined, endpointName);\n }\n\n warn(params: LogParams) {\n const { fnName, payload, endpointName } = params;\n return this.send(\"warn\", fnName, payload, undefined, endpointName);\n }\n\n error(params: LogParams) {\n const { fnName, messageError, payload, endpointName } = params;\n return this.send(\"error\", fnName, payload, messageError, endpointName);\n }\n}\n\nexport const logger = new Logger();\n","import { initializeApp, getApps, FirebaseApp } from \"firebase/app\";\nimport { getFirestore, Firestore } from \"firebase/firestore\";\nimport { LoggerConfig } from \"../types\";\n\nlet app: FirebaseApp;\nlet db: Firestore;\n\nexport const initFirebase = (config: LoggerConfig) => {\n if (!getApps().length) {\n app = initializeApp(\n {\n apiKey: config.apiKey,\n authDomain: config.authDomain,\n projectId: config.projectId,\n storageBucket: config.storageBucket,\n messagingSenderId: config.messagingSenderId,\n appId: config.appId,\n },\n \"ga-logger\",\n );\n } else {\n app = getApps()[0];\n }\n\n db = getFirestore(app);\n return db;\n};\n\nexport const getDb = () => db;\n","import { Component, type ErrorInfo, type ReactNode } from \"react\";\nimport { logger } from \"../core/logger\";\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode; // ← optional custom UI saat crash\n}\n\ninterface State {\n hasError: boolean;\n}\n\nexport class LoggerErrorBoundary extends Component<Props, State> {\n constructor(props: Props) {\n super(props);\n this.state = { hasError: false };\n }\n\n static getDerivedStateFromError(): State {\n return { hasError: true };\n }\n\n componentDidCatch(error: Error, info: ErrorInfo) {\n logger.addBreadcrumb(\"error\", `React crash: ${error.message}`, {\n componentStack: info.componentStack ?? undefined,\n });\n\n logger.error({\n fnName: \"LoggerErrorBoundary\",\n messageError: error,\n payload: {\n componentStack: info.componentStack ?? undefined,\n },\n });\n }\n\n render() {\n if (this.state.hasError) {\n return this.props.fallback ?? (\n <div style={{ padding: \"1rem\", color: \"red\" }}>\n Something went wrong.\n </div>\n );\n }\n\n return this.props.children;\n }\n}"],"mappings":"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","names":["import_firestore","db"]}
|
package/dist/index.mjs
CHANGED
|
@@ -29,10 +29,12 @@ var getDb = () => db;
|
|
|
29
29
|
|
|
30
30
|
// src/core/logger.ts
|
|
31
31
|
var sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
32
|
+
var MAX_BREADCRUMBS = 50;
|
|
32
33
|
var Logger = class {
|
|
33
34
|
constructor() {
|
|
34
35
|
this.config = null;
|
|
35
36
|
this.initialized = false;
|
|
37
|
+
this.breadcrumbs = [];
|
|
36
38
|
}
|
|
37
39
|
init(config) {
|
|
38
40
|
this.config = config;
|
|
@@ -47,6 +49,112 @@ var Logger = class {
|
|
|
47
49
|
}
|
|
48
50
|
this.config.userId = userId;
|
|
49
51
|
}
|
|
52
|
+
// ✅ NEW: Add breadcrumb ke in-memory array
|
|
53
|
+
addBreadcrumb(type, message, data) {
|
|
54
|
+
const breadcrumb = {
|
|
55
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
56
|
+
type,
|
|
57
|
+
message,
|
|
58
|
+
data
|
|
59
|
+
};
|
|
60
|
+
this.breadcrumbs.push(breadcrumb);
|
|
61
|
+
if (this.breadcrumbs.length > MAX_BREADCRUMBS) {
|
|
62
|
+
this.breadcrumbs.shift();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ✅ NEW: Global error capture — hanya aktif di non-development
|
|
66
|
+
initGlobalCapture() {
|
|
67
|
+
if (!this.config || this.config.env === "DEV") {
|
|
68
|
+
console.info("[ga-logger] global capture skipped \u2014 DEV environment");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
window.onerror = (message, source, lineno, colno, error) => {
|
|
72
|
+
if (this.shouldSkipGlobalError(message?.toString(), source)) return false;
|
|
73
|
+
this.addBreadcrumb("error", `window.onerror: ${message}`, {
|
|
74
|
+
source,
|
|
75
|
+
lineno,
|
|
76
|
+
colno
|
|
77
|
+
});
|
|
78
|
+
this.error({
|
|
79
|
+
fnName: "window.onerror",
|
|
80
|
+
messageError: error ?? new Error(String(message)),
|
|
81
|
+
payload: { source, lineno, colno }
|
|
82
|
+
});
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
86
|
+
const message = event.reason?.message ?? String(event.reason);
|
|
87
|
+
if (this.shouldSkipGlobalError(message)) return;
|
|
88
|
+
const error = event.reason instanceof Error ? event.reason : new Error(message);
|
|
89
|
+
this.addBreadcrumb("error", `unhandledrejection: ${message}`);
|
|
90
|
+
this.error({
|
|
91
|
+
fnName: "window.unhandledrejection",
|
|
92
|
+
messageError: error
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
console.info("[ga-logger] global capture active");
|
|
96
|
+
}
|
|
97
|
+
// ✅ NEW: Smart filter — skip noise dari CORS, network, third-party
|
|
98
|
+
shouldSkipGlobalError(message, source) {
|
|
99
|
+
if (!message) return true;
|
|
100
|
+
const noisePatterns = [
|
|
101
|
+
"CORS",
|
|
102
|
+
"cross-origin",
|
|
103
|
+
"ERR_FAILED",
|
|
104
|
+
"Failed to load resource",
|
|
105
|
+
"NetworkError",
|
|
106
|
+
"ResizeObserver loop",
|
|
107
|
+
"Script error"
|
|
108
|
+
];
|
|
109
|
+
const isNoise = noisePatterns.some((pattern) => message.toLowerCase().includes(pattern.toLowerCase()));
|
|
110
|
+
if (isNoise) return true;
|
|
111
|
+
if (source) {
|
|
112
|
+
const thirdPartyPatterns = ["magnolia", "gtm.js", "analytics", "googletagmanager", "facebook", "hotjar"];
|
|
113
|
+
const isThirdParty = thirdPartyPatterns.some((pattern) => source.includes(pattern));
|
|
114
|
+
if (isThirdParty) return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// ✅ NEW: interceptFetch — auto breadcrumb, skip 4xx/5xx ke Firestore
|
|
119
|
+
interceptFetch() {
|
|
120
|
+
if (typeof window === "undefined") return;
|
|
121
|
+
const originalFetch = window.fetch;
|
|
122
|
+
window.fetch = async (input, init) => {
|
|
123
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
124
|
+
const method = init?.method ?? "GET";
|
|
125
|
+
const startTime = Date.now();
|
|
126
|
+
try {
|
|
127
|
+
const response = await originalFetch(input, init);
|
|
128
|
+
const duration = Date.now() - startTime;
|
|
129
|
+
const status = response.status;
|
|
130
|
+
this.addBreadcrumb("fetch", `${method} ${url}`, {
|
|
131
|
+
status,
|
|
132
|
+
duration: `${duration}ms`
|
|
133
|
+
});
|
|
134
|
+
if (status >= 400) {
|
|
135
|
+
console.warn(`[ga-logger] HTTP ${status} skipped \u2014 Kibana handles this`, url);
|
|
136
|
+
}
|
|
137
|
+
return response;
|
|
138
|
+
} catch (networkError) {
|
|
139
|
+
const duration = Date.now() - startTime;
|
|
140
|
+
const error = networkError instanceof Error ? networkError : new Error(String(networkError));
|
|
141
|
+
this.addBreadcrumb("fetch", `${method} ${url} \u2014 NETWORK ERROR`, {
|
|
142
|
+
duration: `${duration}ms`,
|
|
143
|
+
error: error.message
|
|
144
|
+
});
|
|
145
|
+
if (this.config?.env !== "DEV") {
|
|
146
|
+
this.error({
|
|
147
|
+
fnName: "interceptFetch",
|
|
148
|
+
messageError: error,
|
|
149
|
+
endpointName: url,
|
|
150
|
+
payload: { method, duration: `${duration}ms` }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
throw networkError;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
console.info("[ga-logger] fetch intercepted");
|
|
157
|
+
}
|
|
50
158
|
captureAction(params, fn) {
|
|
51
159
|
return Promise.resolve().then(() => fn()).catch((error) => {
|
|
52
160
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -59,6 +167,10 @@ var Logger = class {
|
|
|
59
167
|
console.warn("[ga-logger] not initialized. Call logger.init() first.");
|
|
60
168
|
return;
|
|
61
169
|
}
|
|
170
|
+
if (this.config.env === "DEV") {
|
|
171
|
+
console.error(`[ga-logger][DEV] ${message}`, { payload, error });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
62
174
|
const entry = {
|
|
63
175
|
timestamp: /* @__PURE__ */ new Date(),
|
|
64
176
|
level,
|
|
@@ -70,7 +182,9 @@ var Logger = class {
|
|
|
70
182
|
stackTrace: error?.stack ?? null,
|
|
71
183
|
payload,
|
|
72
184
|
repo: this.config.repo,
|
|
73
|
-
endpoint: endpoint ?? null
|
|
185
|
+
endpoint: endpoint ?? null,
|
|
186
|
+
breadcrumbs: [...this.breadcrumbs]
|
|
187
|
+
// ← snapshot saat error terjadi
|
|
74
188
|
};
|
|
75
189
|
try {
|
|
76
190
|
const db2 = getDb();
|
|
@@ -96,7 +210,39 @@ var Logger = class {
|
|
|
96
210
|
}
|
|
97
211
|
};
|
|
98
212
|
var logger = new Logger();
|
|
213
|
+
|
|
214
|
+
// src/components/LoggerErrorBoundary.tsx
|
|
215
|
+
import { Component } from "react";
|
|
216
|
+
import { jsx } from "react/jsx-runtime";
|
|
217
|
+
var LoggerErrorBoundary = class extends Component {
|
|
218
|
+
constructor(props) {
|
|
219
|
+
super(props);
|
|
220
|
+
this.state = { hasError: false };
|
|
221
|
+
}
|
|
222
|
+
static getDerivedStateFromError() {
|
|
223
|
+
return { hasError: true };
|
|
224
|
+
}
|
|
225
|
+
componentDidCatch(error, info) {
|
|
226
|
+
logger.addBreadcrumb("error", `React crash: ${error.message}`, {
|
|
227
|
+
componentStack: info.componentStack ?? void 0
|
|
228
|
+
});
|
|
229
|
+
logger.error({
|
|
230
|
+
fnName: "LoggerErrorBoundary",
|
|
231
|
+
messageError: error,
|
|
232
|
+
payload: {
|
|
233
|
+
componentStack: info.componentStack ?? void 0
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
render() {
|
|
238
|
+
if (this.state.hasError) {
|
|
239
|
+
return this.props.fallback ?? /* @__PURE__ */ jsx("div", { style: { padding: "1rem", color: "red" }, children: "Something went wrong." });
|
|
240
|
+
}
|
|
241
|
+
return this.props.children;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
99
244
|
export {
|
|
245
|
+
LoggerErrorBoundary,
|
|
100
246
|
logger
|
|
101
247
|
};
|
|
102
248
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/logger.ts","../src/config/firebase.ts"],"sourcesContent":["import { collection, addDoc, Timestamp } from \"firebase/firestore\";\nimport { initFirebase, getDb } from \"../config/firebase\";\nimport type { LogEntry, LogLevel, LogParams, LoggerConfig } from \"../types\";\n\nconst sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n\nclass Logger {\n private config: LoggerConfig | null = null;\n private initialized = false;\n\n init(config: LoggerConfig) {\n this.config = config;\n initFirebase(config);\n this.initialized = true;\n console.info(\"[ga-logger] initialized —\", config.env);\n }\n\n setUserId(userId: string | null) {\n if (!this.config) {\n console.warn(\"[ga-logger] not initialized. Call logger.init() first.\");\n return;\n }\n this.config.userId = userId;\n }\n \n captureAction<T>(params: LogParams, fn: () => T | Promise<T>): Promise<T | undefined> {\n return Promise.resolve()\n .then(() => fn())\n .catch((error: unknown) => {\n const err = error instanceof Error ? error : new Error(String(error));\n this.send(\"error\", params.fnName, params.payload, err, params.endpointName);\n return undefined;\n });\n }\n private async send(\n level: LogLevel,\n message: string,\n payload: Record<string, unknown> = {},\n error?: Error,\n endpoint?: string,\n ) {\n if (!this.initialized || !this.config) {\n console.warn(\"[ga-logger] not initialized. Call logger.init() first.\");\n return;\n }\n\n const entry: LogEntry = {\n timestamp: new Date(),\n level,\n message,\n env: this.config.env,\n userId: this.config.userId ?? null,\n sessionId,\n page: typeof window !== \"undefined\" ? window.location.pathname : \"\",\n stackTrace: error?.stack ?? null,\n payload,\n repo: this.config.repo,\n endpoint: endpoint ?? null,\n };\n try {\n const db = getDb();\n await addDoc(collection(db, \"logs\"), {\n ...entry,\n timestamp: Timestamp.fromDate(entry.timestamp),\n });\n } catch (e) {\n console.error(\"[ga-logger] failed to send log:\", e);\n }\n }\n\n info(params: LogParams) {\n const { fnName, payload, endpointName } = params;\n return this.send(\"info\", fnName, payload, undefined, endpointName);\n }\n\n warn(params: LogParams) {\n const { fnName, payload, endpointName } = params;\n return this.send(\"warn\", fnName, payload, undefined, endpointName);\n }\n\n error(params: LogParams) {\n const { fnName, messageError, payload, endpointName } = params;\n return this.send(\"error\", fnName, payload, messageError, endpointName);\n }\n}\n\n\n\nexport const logger = new Logger();","import { initializeApp, getApps, FirebaseApp } from \"firebase/app\";\nimport { getFirestore, Firestore } from \"firebase/firestore\";\nimport { LoggerConfig } from \"../types\";\n\nlet app: FirebaseApp;\nlet db: Firestore;\n\nexport const initFirebase = (config: LoggerConfig) => {\n if (!getApps().length) {\n app = initializeApp(\n {\n apiKey: config.apiKey,\n authDomain: config.authDomain,\n projectId: config.projectId,\n storageBucket: config.storageBucket,\n messagingSenderId: config.messagingSenderId,\n appId: config.appId,\n },\n \"ga-logger\",\n );\n } else {\n app = getApps()[0];\n }\n\n db = getFirestore(app);\n return db;\n};\n\nexport const getDb = () => db;\n"],"mappings":"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","names":["db"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/logger.ts","../src/config/firebase.ts","../src/components/LoggerErrorBoundary.tsx"],"sourcesContent":["import { collection, addDoc, Timestamp } from \"firebase/firestore\";\nimport { initFirebase, getDb } from \"../config/firebase\";\nimport type { Breadcrumb, BreadcrumbType, LogEntry, LogLevel, LogParams, LoggerConfig } from \"../types\";\n\nconst sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\nconst MAX_BREADCRUMBS = 50;\n\nclass Logger {\n private config: LoggerConfig | null = null;\n private initialized = false;\n private breadcrumbs: Breadcrumb[] = [];\n\n init(config: LoggerConfig) {\n this.config = config;\n initFirebase(config);\n this.initialized = true;\n console.info(\"[ga-logger] initialized —\", config.env);\n }\n\n setUserId(userId: string | null) {\n if (!this.config) {\n console.warn(\"[ga-logger] not initialized. Call logger.init() first.\");\n return;\n }\n this.config.userId = userId;\n }\n\n // ✅ NEW: Add breadcrumb ke in-memory array\n addBreadcrumb(type: BreadcrumbType, message: string, data?: Record<string, unknown>) {\n const breadcrumb: Breadcrumb = {\n timestamp: new Date(),\n type,\n message,\n data,\n };\n\n this.breadcrumbs.push(breadcrumb);\n\n // Jaga memory — max 50 breadcrumbs, buang yang paling lama\n if (this.breadcrumbs.length > MAX_BREADCRUMBS) {\n this.breadcrumbs.shift();\n }\n }\n\n // ✅ NEW: Global error capture — hanya aktif di non-development\n initGlobalCapture() {\n if (!this.config || this.config.env === \"DEV\") {\n console.info(\"[ga-logger] global capture skipped — DEV environment\");\n return;\n }\n\n // Capture JS runtime error\n window.onerror = (message, source, lineno, colno, error) => {\n if (this.shouldSkipGlobalError(message?.toString(), source)) return false;\n\n this.addBreadcrumb(\"error\", `window.onerror: ${message}`, {\n source,\n lineno,\n colno,\n });\n\n this.error({\n fnName: \"window.onerror\",\n messageError: error ?? new Error(String(message)),\n payload: { source, lineno, colno },\n });\n\n return false;\n };\n\n // Capture unhandled Promise rejection\n window.addEventListener(\"unhandledrejection\", (event) => {\n const message = event.reason?.message ?? String(event.reason);\n\n if (this.shouldSkipGlobalError(message)) return;\n\n const error = event.reason instanceof Error ? event.reason : new Error(message);\n\n this.addBreadcrumb(\"error\", `unhandledrejection: ${message}`);\n\n this.error({\n fnName: \"window.unhandledrejection\",\n messageError: error,\n });\n });\n\n console.info(\"[ga-logger] global capture active\");\n }\n\n // ✅ NEW: Smart filter — skip noise dari CORS, network, third-party\n private shouldSkipGlobalError(message?: string, source?: string): boolean {\n if (!message) return true;\n\n const noisePatterns = [\n \"CORS\",\n \"cross-origin\",\n \"ERR_FAILED\",\n \"Failed to load resource\",\n \"NetworkError\",\n \"ResizeObserver loop\",\n \"Script error\",\n ];\n\n const isNoise = noisePatterns.some((pattern) => message.toLowerCase().includes(pattern.toLowerCase()));\n if (isNoise) return true;\n\n // Skip error dari third-party source\n if (source) {\n const thirdPartyPatterns = [\"magnolia\", \"gtm.js\", \"analytics\", \"googletagmanager\", \"facebook\", \"hotjar\"];\n const isThirdParty = thirdPartyPatterns.some((pattern) => source.includes(pattern));\n if (isThirdParty) return true;\n }\n\n return false;\n }\n\n // ✅ NEW: interceptFetch — auto breadcrumb, skip 4xx/5xx ke Firestore\n interceptFetch() {\n if (typeof window === \"undefined\") return;\n\n const originalFetch = window.fetch;\n\n window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n const method = init?.method ?? \"GET\";\n const startTime = Date.now();\n\n try {\n const response = await originalFetch(input, init);\n const duration = Date.now() - startTime;\n const status = response.status;\n\n // Selalu add breadcrumb untuk semua fetch\n this.addBreadcrumb(\"fetch\", `${method} ${url}`, {\n status,\n duration: `${duration}ms`,\n });\n\n // 4xx/5xx → skip logger.error, Kibana sudah handle\n if (status >= 400) {\n console.warn(`[ga-logger] HTTP ${status} skipped — Kibana handles this`, url);\n }\n\n return response;\n } catch (networkError) {\n const duration = Date.now() - startTime;\n const error = networkError instanceof Error ? networkError : new Error(String(networkError));\n\n // Network failure → Kibana tidak tahu, kita log!\n this.addBreadcrumb(\"fetch\", `${method} ${url} — NETWORK ERROR`, {\n duration: `${duration}ms`,\n error: error.message,\n });\n\n // Hanya log jika bukan DEV\n if (this.config?.env !== \"DEV\") {\n this.error({\n fnName: \"interceptFetch\",\n messageError: error,\n endpointName: url,\n payload: { method, duration: `${duration}ms` },\n });\n }\n\n throw networkError;\n }\n };\n\n console.info(\"[ga-logger] fetch intercepted\");\n }\n\n captureAction<T>(params: LogParams, fn: () => T | Promise<T>): Promise<T | undefined> {\n return Promise.resolve()\n .then(() => fn())\n .catch((error: unknown) => {\n const err = error instanceof Error ? error : new Error(String(error));\n this.send(\"error\", params.fnName, params.payload, err, params.endpointName);\n return undefined;\n });\n }\n\n private async send(\n level: LogLevel,\n message: string,\n payload: Record<string, unknown> = {},\n error?: Error,\n endpoint?: string,\n ) {\n if (!this.initialized || !this.config) {\n console.warn(\"[ga-logger] not initialized. Call logger.init() first.\");\n return;\n }\n\n // DEV → console only, skip Firestore\n if (this.config.env === \"DEV\") {\n console.error(`[ga-logger][DEV] ${message}`, { payload, error });\n return;\n }\n\n const entry: LogEntry = {\n timestamp: new Date(),\n level,\n message,\n env: this.config.env,\n userId: this.config.userId ?? null,\n sessionId,\n page: typeof window !== \"undefined\" ? window.location.pathname : \"\",\n stackTrace: error?.stack ?? null,\n payload,\n repo: this.config.repo,\n endpoint: endpoint ?? null,\n breadcrumbs: [...this.breadcrumbs], // ← snapshot saat error terjadi\n };\n\n try {\n const db = getDb();\n await addDoc(collection(db, \"logs\"), {\n ...entry,\n timestamp: Timestamp.fromDate(entry.timestamp),\n });\n } catch (e) {\n console.error(\"[ga-logger] failed to send log:\", e);\n }\n }\n\n info(params: LogParams) {\n const { fnName, payload, endpointName } = params;\n return this.send(\"info\", fnName, payload, undefined, endpointName);\n }\n\n warn(params: LogParams) {\n const { fnName, payload, endpointName } = params;\n return this.send(\"warn\", fnName, payload, undefined, endpointName);\n }\n\n error(params: LogParams) {\n const { fnName, messageError, payload, endpointName } = params;\n return this.send(\"error\", fnName, payload, messageError, endpointName);\n }\n}\n\nexport const logger = new Logger();\n","import { initializeApp, getApps, FirebaseApp } from \"firebase/app\";\nimport { getFirestore, Firestore } from \"firebase/firestore\";\nimport { LoggerConfig } from \"../types\";\n\nlet app: FirebaseApp;\nlet db: Firestore;\n\nexport const initFirebase = (config: LoggerConfig) => {\n if (!getApps().length) {\n app = initializeApp(\n {\n apiKey: config.apiKey,\n authDomain: config.authDomain,\n projectId: config.projectId,\n storageBucket: config.storageBucket,\n messagingSenderId: config.messagingSenderId,\n appId: config.appId,\n },\n \"ga-logger\",\n );\n } else {\n app = getApps()[0];\n }\n\n db = getFirestore(app);\n return db;\n};\n\nexport const getDb = () => db;\n","import { Component, type ErrorInfo, type ReactNode } from \"react\";\nimport { logger } from \"../core/logger\";\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode; // ← optional custom UI saat crash\n}\n\ninterface State {\n hasError: boolean;\n}\n\nexport class LoggerErrorBoundary extends Component<Props, State> {\n constructor(props: Props) {\n super(props);\n this.state = { hasError: false };\n }\n\n static getDerivedStateFromError(): State {\n return { hasError: true };\n }\n\n componentDidCatch(error: Error, info: ErrorInfo) {\n logger.addBreadcrumb(\"error\", `React crash: ${error.message}`, {\n componentStack: info.componentStack ?? undefined,\n });\n\n logger.error({\n fnName: \"LoggerErrorBoundary\",\n messageError: error,\n payload: {\n componentStack: info.componentStack ?? undefined,\n },\n });\n }\n\n render() {\n if (this.state.hasError) {\n return this.props.fallback ?? (\n <div style={{ padding: \"1rem\", color: \"red\" }}>\n Something went wrong.\n </div>\n );\n }\n\n return this.props.children;\n }\n}"],"mappings":"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","names":["db"]}
|