@vtex/faststore-plugin-buyer-portal 1.3.12 → 1.3.13
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/CHANGELOG.md +12 -2
- package/package.json +1 -1
- package/src/features/shared/components/ErrorBoundary/ErrorBoundary.tsx +21 -2
- package/src/features/shared/hooks/index.ts +1 -0
- package/src/features/shared/hooks/useLogger.ts +72 -0
- package/src/features/shared/services/index.ts +1 -0
- package/src/features/shared/services/logger/constants.ts +49 -0
- package/src/features/shared/services/logger/context.ts +46 -0
- package/src/features/shared/services/logger/index.ts +4 -0
- package/src/features/shared/services/logger/otlp-logger.service.ts +241 -0
- package/src/features/shared/services/logger/types.ts +114 -0
- package/src/features/shared/utils/constants.ts +1 -1
- package/src/features/shared/utils/withClientErrorBoundary.ts +34 -13
- package/src/features/shared/utils/withLoaderErrorBoundary.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.13] - 2025-11-03
|
|
11
|
+
|
|
12
|
+
- Setup OTLP to send log events on error boundary
|
|
13
|
+
|
|
10
14
|
## [1.3.12] - 2025-10-30
|
|
11
15
|
|
|
12
16
|
### Added
|
|
@@ -171,7 +175,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
171
175
|
- Add CHANGELOG file
|
|
172
176
|
- Add README file
|
|
173
177
|
|
|
174
|
-
[unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.
|
|
178
|
+
[unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.13...HEAD
|
|
175
179
|
[1.2.3]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.2.2...1.2.3
|
|
176
180
|
[1.2.3]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.2.3
|
|
177
181
|
[1.2.4]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.2.4
|
|
@@ -183,5 +187,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
183
187
|
[1.3.9]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.8...v1.3.9
|
|
184
188
|
[1.3.8]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.7...v1.3.8
|
|
185
189
|
[1.3.7]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.3.7
|
|
190
|
+
|
|
191
|
+
# <<<<<<< HEAD
|
|
192
|
+
|
|
193
|
+
[1.3.13]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.12...v1.3.13
|
|
186
194
|
[1.3.12]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.11...v1.3.12
|
|
187
|
-
|
|
195
|
+
|
|
196
|
+
> > > > > > > main
|
|
197
|
+
> > > > > > > [1.3.11]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.3.11
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { Component, type ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
import { ErrorTabsLayout } from "../../layouts/ErrorTabsLayout/ErrorTabsLayout";
|
|
6
|
+
import { logError } from "../../services/logger";
|
|
6
7
|
import { isPluginError } from "../../utils/isPluginError";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
@@ -35,8 +36,26 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
38
|
-
if (isPluginError(error)
|
|
39
|
-
|
|
39
|
+
if (isPluginError(error)) {
|
|
40
|
+
// Automatically send error logs to OTLP endpoint
|
|
41
|
+
const metadata = {
|
|
42
|
+
"error.message": error.message,
|
|
43
|
+
"error.name": error.name,
|
|
44
|
+
"error.stack": error.stack || "No stack trace available",
|
|
45
|
+
"error.component": this.props.tags?.component || "Unknown",
|
|
46
|
+
"error.type": this.props.tags?.errorType || "Unknown",
|
|
47
|
+
"error.info":
|
|
48
|
+
errorInfo.componentStack || "No component stack available",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
Promise.resolve().then(() =>
|
|
52
|
+
logError(`ErrorBoundary caught error: ${error.message}`, metadata)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Call custom onError handler if provided
|
|
56
|
+
if (this.props.onError) {
|
|
57
|
+
this.props.onError(error, errorInfo);
|
|
58
|
+
}
|
|
40
59
|
}
|
|
41
60
|
}
|
|
42
61
|
|
|
@@ -12,4 +12,5 @@ export { useAddToScope } from "./useAddToScope";
|
|
|
12
12
|
export { useRemoveFromScope } from "./useRemoveFromScope";
|
|
13
13
|
export { usePageItems, type UsePageItemsProps } from "./usePageItems";
|
|
14
14
|
export { useRouterLoading } from "./useRouterLoading";
|
|
15
|
+
export { useLogger } from "./useLogger";
|
|
15
16
|
export { useGetDependenciesVersion } from "./useGetDependenciesVersion";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
|
|
3
|
+
import { BuyerPortalContext } from "../components/BuyerPortalProvider/BuyerPortalProvider";
|
|
4
|
+
import { logError, logInfo, logWarn } from "../services/logger";
|
|
5
|
+
|
|
6
|
+
import type { LogContext, LogMetadata } from "../services/logger";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to access logger functionality with automatic context enrichment
|
|
10
|
+
* from BuyerPortalContext
|
|
11
|
+
*/
|
|
12
|
+
export function useLogger() {
|
|
13
|
+
const buyerPortalContext = useContext(BuyerPortalContext);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get additional context from BuyerPortalContext if available
|
|
17
|
+
*/
|
|
18
|
+
const getAdditionalContext = (): Partial<LogContext> => {
|
|
19
|
+
if (!buyerPortalContext) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const context: Partial<LogContext> = {};
|
|
24
|
+
|
|
25
|
+
if (buyerPortalContext.currentOrgUnit?.id) {
|
|
26
|
+
context.orgUnitId = buyerPortalContext.currentOrgUnit.id;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (buyerPortalContext.currentUser?.id) {
|
|
30
|
+
context.userId = buyerPortalContext.currentUser.id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (buyerPortalContext.clientContext?.customerId) {
|
|
34
|
+
context.customerId = buyerPortalContext.clientContext.customerId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return context;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Log an error message with optional metadata
|
|
42
|
+
*/
|
|
43
|
+
const error = (message: string, metadata?: LogMetadata): Promise<void> => {
|
|
44
|
+
const context = getAdditionalContext();
|
|
45
|
+
|
|
46
|
+
return logError(message, metadata, context);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Log a warning message with optional metadata
|
|
51
|
+
*/
|
|
52
|
+
const warn = (message: string, metadata?: LogMetadata): Promise<void> => {
|
|
53
|
+
const context = getAdditionalContext();
|
|
54
|
+
|
|
55
|
+
return logWarn(message, metadata, context);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Log an info message with optional metadata
|
|
60
|
+
*/
|
|
61
|
+
const info = (message: string, metadata?: LogMetadata): Promise<void> => {
|
|
62
|
+
const context = getAdditionalContext();
|
|
63
|
+
|
|
64
|
+
return logInfo(message, metadata, context);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
error,
|
|
69
|
+
warn,
|
|
70
|
+
info,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CURRENT_VERSION } from "../../utils/constants";
|
|
2
|
+
|
|
3
|
+
import { LogLevel, SeverityNumber } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OTLP Endpoints for VTEX Observability
|
|
7
|
+
*/
|
|
8
|
+
export const OTLP_ENDPOINTS = {
|
|
9
|
+
PRODUCTION: "https://stable.vtexobservability.com/v1/logs",
|
|
10
|
+
DEVELOPMENT: "https://beta.vtexobservability.com/v1/logs",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Service resource attributes
|
|
15
|
+
*/
|
|
16
|
+
export const SERVICE_NAME = "faststore-plugin-buyer-portal";
|
|
17
|
+
export const SERVICE_VERSION = CURRENT_VERSION;
|
|
18
|
+
export const SERVICE_INDEX = "b2b_organization_account";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Scope name for logs
|
|
22
|
+
*/
|
|
23
|
+
export const SCOPE_NAME = "buyer-portal-web";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Map log levels to OTLP severity numbers
|
|
27
|
+
*/
|
|
28
|
+
export const SEVERITY_MAP: Record<LogLevel, SeverityNumber> = {
|
|
29
|
+
INFO: SeverityNumber.INFO,
|
|
30
|
+
WARN: SeverityNumber.WARN,
|
|
31
|
+
ERROR: SeverityNumber.ERROR,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Attribute keys for structured logging
|
|
36
|
+
*/
|
|
37
|
+
export const LOG_ATTRIBUTES = {
|
|
38
|
+
ERROR_MESSAGE: "error.message",
|
|
39
|
+
ERROR_STACK: "error.stack",
|
|
40
|
+
ERROR_COMPONENT: "error.component",
|
|
41
|
+
ERROR_TYPE: "error.type",
|
|
42
|
+
ACCOUNT: "account",
|
|
43
|
+
ENVIRONMENT: "environment",
|
|
44
|
+
URL: "url",
|
|
45
|
+
USER_AGENT: "user_agent",
|
|
46
|
+
ORG_UNIT_ID: "org_unit_id",
|
|
47
|
+
USER_ID: "user_id",
|
|
48
|
+
CUSTOMER_ID: "customer_id",
|
|
49
|
+
} as const;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import storeConfig from "discovery.config";
|
|
2
|
+
|
|
3
|
+
import { isDevelopment } from "../../utils/environment";
|
|
4
|
+
|
|
5
|
+
import type { LogContext } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collect log context from the application environment
|
|
9
|
+
* Gathers account, environment, URL, user agent, and other metadata
|
|
10
|
+
*/
|
|
11
|
+
export function collectLogContext(
|
|
12
|
+
additionalContext?: Partial<LogContext>
|
|
13
|
+
): LogContext {
|
|
14
|
+
const context: LogContext = {
|
|
15
|
+
environment: isDevelopment() ? "development" : "production",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Collect browser context only if in browser environment
|
|
19
|
+
if (typeof window !== "undefined") {
|
|
20
|
+
context.url = window.location.href;
|
|
21
|
+
context.userAgent = navigator.userAgent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get VTEX account from store config
|
|
25
|
+
if (storeConfig?.api?.storeId) {
|
|
26
|
+
context.account = storeConfig.api.storeId;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Merge with additional context provided
|
|
30
|
+
if (additionalContext) {
|
|
31
|
+
Object.assign(context, additionalContext);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get current timestamp in Unix nanoseconds (OTLP format)
|
|
39
|
+
* JavaScript Date.now() returns milliseconds, so we multiply by 1,000,000
|
|
40
|
+
*/
|
|
41
|
+
export function getTimestampNano(): string {
|
|
42
|
+
const milliseconds = Date.now();
|
|
43
|
+
const nanoseconds = milliseconds * 1_000_000;
|
|
44
|
+
|
|
45
|
+
return nanoseconds.toString();
|
|
46
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { isDevelopment } from "../../utils/environment";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LOG_ATTRIBUTES,
|
|
5
|
+
OTLP_ENDPOINTS,
|
|
6
|
+
SCOPE_NAME,
|
|
7
|
+
SERVICE_INDEX,
|
|
8
|
+
SERVICE_NAME,
|
|
9
|
+
SERVICE_VERSION,
|
|
10
|
+
SEVERITY_MAP,
|
|
11
|
+
} from "./constants";
|
|
12
|
+
import { collectLogContext, getTimestampNano } from "./context";
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
LogContext,
|
|
16
|
+
LogMetadata,
|
|
17
|
+
LogParams,
|
|
18
|
+
OTLPKeyValue,
|
|
19
|
+
OTLPLogsPayload,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert metadata object to OTLP key-value attributes
|
|
24
|
+
*/
|
|
25
|
+
function metadataToAttributes(metadata?: LogMetadata): OTLPKeyValue[] {
|
|
26
|
+
if (!metadata) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return Object.entries(metadata).map(([key, value]) => {
|
|
31
|
+
// Handle different value types
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
return { key, value: { stringValue: value } };
|
|
34
|
+
} else if (typeof value === "number") {
|
|
35
|
+
if (Number.isInteger(value)) {
|
|
36
|
+
return { key, value: { intValue: value } };
|
|
37
|
+
} else {
|
|
38
|
+
return { key, value: { doubleValue: value } };
|
|
39
|
+
}
|
|
40
|
+
} else if (typeof value === "boolean") {
|
|
41
|
+
return { key, value: { boolValue: value } };
|
|
42
|
+
} else {
|
|
43
|
+
// Convert complex objects to JSON string
|
|
44
|
+
return { key, value: { stringValue: JSON.stringify(value) } };
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert log context to OTLP attributes
|
|
51
|
+
*/
|
|
52
|
+
function contextToAttributes(context: LogContext): OTLPKeyValue[] {
|
|
53
|
+
const attributes: OTLPKeyValue[] = [];
|
|
54
|
+
|
|
55
|
+
if (context.account) {
|
|
56
|
+
attributes.push({
|
|
57
|
+
key: LOG_ATTRIBUTES.ACCOUNT,
|
|
58
|
+
value: { stringValue: context.account },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (context.environment) {
|
|
63
|
+
attributes.push({
|
|
64
|
+
key: LOG_ATTRIBUTES.ENVIRONMENT,
|
|
65
|
+
value: { stringValue: context.environment },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (context.url) {
|
|
70
|
+
attributes.push({
|
|
71
|
+
key: LOG_ATTRIBUTES.URL,
|
|
72
|
+
value: { stringValue: context.url },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (context.userAgent) {
|
|
77
|
+
attributes.push({
|
|
78
|
+
key: LOG_ATTRIBUTES.USER_AGENT,
|
|
79
|
+
value: { stringValue: context.userAgent },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (context.orgUnitId) {
|
|
84
|
+
attributes.push({
|
|
85
|
+
key: LOG_ATTRIBUTES.ORG_UNIT_ID,
|
|
86
|
+
value: { stringValue: context.orgUnitId },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (context.userId) {
|
|
91
|
+
attributes.push({
|
|
92
|
+
key: LOG_ATTRIBUTES.USER_ID,
|
|
93
|
+
value: { stringValue: context.userId },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (context.customerId) {
|
|
98
|
+
attributes.push({
|
|
99
|
+
key: LOG_ATTRIBUTES.CUSTOMER_ID,
|
|
100
|
+
value: { stringValue: context.customerId },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return attributes;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build OTLP logs payload
|
|
109
|
+
*/
|
|
110
|
+
function buildOTLPPayload(params: LogParams): OTLPLogsPayload {
|
|
111
|
+
const { message, level, metadata, context } = params;
|
|
112
|
+
|
|
113
|
+
// Collect full context
|
|
114
|
+
const fullContext = collectLogContext(context);
|
|
115
|
+
|
|
116
|
+
// Build attributes from context and metadata
|
|
117
|
+
const contextAttributes = contextToAttributes(fullContext);
|
|
118
|
+
const metadataAttributes = metadataToAttributes(metadata);
|
|
119
|
+
const allAttributes = [...contextAttributes, ...metadataAttributes];
|
|
120
|
+
|
|
121
|
+
// Build OTLP payload structure
|
|
122
|
+
const payload: OTLPLogsPayload = {
|
|
123
|
+
resourceLogs: [
|
|
124
|
+
{
|
|
125
|
+
resource: {
|
|
126
|
+
attributes: [
|
|
127
|
+
{
|
|
128
|
+
key: "service.name",
|
|
129
|
+
value: { stringValue: SERVICE_NAME },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: "service.version",
|
|
133
|
+
value: { stringValue: SERVICE_VERSION },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: "vtex.search_index",
|
|
137
|
+
value: { stringValue: SERVICE_INDEX },
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
scopeLogs: [
|
|
142
|
+
{
|
|
143
|
+
scope: {
|
|
144
|
+
name: SCOPE_NAME,
|
|
145
|
+
version: SERVICE_VERSION,
|
|
146
|
+
},
|
|
147
|
+
logRecords: [
|
|
148
|
+
{
|
|
149
|
+
timeUnixNano: getTimestampNano(),
|
|
150
|
+
severityNumber: SEVERITY_MAP[level],
|
|
151
|
+
severityText: level,
|
|
152
|
+
body: {
|
|
153
|
+
stringValue: message,
|
|
154
|
+
},
|
|
155
|
+
attributes: allAttributes,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return payload;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get the appropriate OTLP endpoint based on environment
|
|
169
|
+
*/
|
|
170
|
+
function getEndpoint(): string {
|
|
171
|
+
return isDevelopment()
|
|
172
|
+
? OTLP_ENDPOINTS.DEVELOPMENT
|
|
173
|
+
: OTLP_ENDPOINTS.PRODUCTION;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Send log to OTLP endpoint
|
|
178
|
+
* This function handles errors silently to prevent logging from breaking the app
|
|
179
|
+
*/
|
|
180
|
+
export async function sendOTLPLog(params: LogParams): Promise<void> {
|
|
181
|
+
try {
|
|
182
|
+
const payload = buildOTLPPayload(params);
|
|
183
|
+
const endpoint = getEndpoint();
|
|
184
|
+
|
|
185
|
+
const response = await fetch(endpoint, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify(payload),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
// Only log to console in development
|
|
195
|
+
if (isDevelopment()) {
|
|
196
|
+
console.warn(
|
|
197
|
+
`Failed to send log to OTLP endpoint: ${response.status} ${response.statusText}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
// Silently catch errors to prevent logging from breaking the app
|
|
203
|
+
// Only log to console in development
|
|
204
|
+
if (isDevelopment()) {
|
|
205
|
+
console.warn("Error sending log to OTLP endpoint:", error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Convenience function to log an error
|
|
212
|
+
*/
|
|
213
|
+
export function logError(
|
|
214
|
+
message: string,
|
|
215
|
+
metadata?: LogMetadata,
|
|
216
|
+
context?: Partial<LogContext>
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
return sendOTLPLog({ message, level: "ERROR", metadata, context });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Convenience function to log a warning
|
|
223
|
+
*/
|
|
224
|
+
export function logWarn(
|
|
225
|
+
message: string,
|
|
226
|
+
metadata?: LogMetadata,
|
|
227
|
+
context?: Partial<LogContext>
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
return sendOTLPLog({ message, level: "WARN", metadata, context });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Convenience function to log info
|
|
234
|
+
*/
|
|
235
|
+
export function logInfo(
|
|
236
|
+
message: string,
|
|
237
|
+
metadata?: LogMetadata,
|
|
238
|
+
context?: Partial<LogContext>
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
return sendOTLPLog({ message, level: "INFO", metadata, context });
|
|
241
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTLP Log Types
|
|
3
|
+
* Based on OpenTelemetry Protocol Specification for Logs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LogLevel = "ERROR" | "WARN" | "INFO";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OTLP Severity Number mapping
|
|
10
|
+
* https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
|
11
|
+
*/
|
|
12
|
+
export enum SeverityNumber {
|
|
13
|
+
UNSPECIFIED = 0,
|
|
14
|
+
INFO = 9,
|
|
15
|
+
WARN = 13,
|
|
16
|
+
ERROR = 17,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* OTLP Attribute value types
|
|
21
|
+
*/
|
|
22
|
+
export type OTLPAttributeValue = {
|
|
23
|
+
stringValue?: string;
|
|
24
|
+
intValue?: number;
|
|
25
|
+
doubleValue?: number;
|
|
26
|
+
boolValue?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* OTLP Key-Value pair for attributes
|
|
31
|
+
*/
|
|
32
|
+
export type OTLPKeyValue = {
|
|
33
|
+
key: string;
|
|
34
|
+
value: OTLPAttributeValue;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* OTLP Log Record structure
|
|
39
|
+
*/
|
|
40
|
+
export type OTLPLogRecord = {
|
|
41
|
+
timeUnixNano: string;
|
|
42
|
+
severityNumber: SeverityNumber;
|
|
43
|
+
severityText: LogLevel;
|
|
44
|
+
body: {
|
|
45
|
+
stringValue: string;
|
|
46
|
+
};
|
|
47
|
+
attributes: OTLPKeyValue[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* OTLP Resource structure
|
|
52
|
+
*/
|
|
53
|
+
export type OTLPResource = {
|
|
54
|
+
attributes: OTLPKeyValue[];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* OTLP Scope structure
|
|
59
|
+
*/
|
|
60
|
+
export type OTLPScope = {
|
|
61
|
+
name: string;
|
|
62
|
+
version?: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* OTLP Scope Logs structure
|
|
67
|
+
*/
|
|
68
|
+
export type OTLPScopeLogs = {
|
|
69
|
+
scope: OTLPScope;
|
|
70
|
+
logRecords: OTLPLogRecord[];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* OTLP Resource Logs structure
|
|
75
|
+
*/
|
|
76
|
+
export type OTLPResourceLogs = {
|
|
77
|
+
resource: OTLPResource;
|
|
78
|
+
scopeLogs: OTLPScopeLogs[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* OTLP Logs Payload (top-level structure)
|
|
83
|
+
*/
|
|
84
|
+
export type OTLPLogsPayload = {
|
|
85
|
+
resourceLogs: OTLPResourceLogs[];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Log metadata that can be passed to logger functions
|
|
90
|
+
*/
|
|
91
|
+
export type LogMetadata = Record<string, unknown>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Log context collected from the application
|
|
95
|
+
*/
|
|
96
|
+
export type LogContext = {
|
|
97
|
+
account?: string;
|
|
98
|
+
environment: "production" | "development";
|
|
99
|
+
url?: string;
|
|
100
|
+
userAgent?: string;
|
|
101
|
+
orgUnitId?: string;
|
|
102
|
+
userId?: string;
|
|
103
|
+
customerId?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parameters for creating a log entry
|
|
108
|
+
*/
|
|
109
|
+
export type LogParams = {
|
|
110
|
+
message: string;
|
|
111
|
+
level: LogLevel;
|
|
112
|
+
metadata?: LogMetadata;
|
|
113
|
+
context?: Partial<LogContext>;
|
|
114
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { logError } from "../services/logger";
|
|
2
|
+
|
|
1
3
|
import { goHome } from "./getHome";
|
|
2
4
|
|
|
3
5
|
import type { ClientError } from "../clients/Client";
|
|
@@ -32,21 +34,40 @@ export function withClientErrorBoundary<TArgs extends unknown[], TReturn>(
|
|
|
32
34
|
try {
|
|
33
35
|
return await clientFunction(...args);
|
|
34
36
|
} catch (error) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
const errorObj = error as Error;
|
|
38
|
+
const clientError = error as Partial<ClientError>;
|
|
39
|
+
|
|
40
|
+
// Extract request information
|
|
41
|
+
const requestInfo = {
|
|
42
|
+
url: typeof clientError.url === "string" ? clientError.url : "unknown",
|
|
43
|
+
method:
|
|
44
|
+
typeof clientError.method === "string"
|
|
45
|
+
? clientError.method
|
|
46
|
+
: "unknown",
|
|
47
|
+
data:
|
|
48
|
+
clientError.responseData !== undefined
|
|
49
|
+
? clientError.responseData
|
|
50
|
+
: undefined,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
Promise.resolve().then(() =>
|
|
54
|
+
logError(`Client error: ${errorObj.message}`, {
|
|
55
|
+
"error.message": errorObj.message,
|
|
56
|
+
"error.name": errorObj.name,
|
|
57
|
+
"error.stack": errorObj.stack || "No stack trace available",
|
|
58
|
+
"error.component": componentName || "Unknown",
|
|
59
|
+
"error.type": "client_error",
|
|
60
|
+
"client.url": requestInfo.url,
|
|
61
|
+
"client.method": requestInfo.method,
|
|
62
|
+
"client.status": clientError.status?.toString() || "unknown",
|
|
63
|
+
"client.response_data":
|
|
64
|
+
requestInfo.data !== undefined
|
|
65
|
+
? JSON.stringify(requestInfo.data)
|
|
47
66
|
: undefined,
|
|
48
|
-
}
|
|
67
|
+
})
|
|
68
|
+
);
|
|
49
69
|
|
|
70
|
+
if (onError && error instanceof Error) {
|
|
50
71
|
onError(error as ClientError, requestInfo);
|
|
51
72
|
}
|
|
52
73
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { logError } from "../services/logger";
|
|
2
|
+
|
|
1
3
|
import { getClientContext } from "./getClientContext";
|
|
2
4
|
import { serializeLoaderData } from "./serializeLoaderData";
|
|
3
5
|
|
|
@@ -25,6 +27,20 @@ export function withLoaderErrorBoundary<TQuery, TReturn>(
|
|
|
25
27
|
query: data.query,
|
|
26
28
|
});
|
|
27
29
|
|
|
30
|
+
// Log error to OTLP endpoint
|
|
31
|
+
const errorObj = error as Error;
|
|
32
|
+
|
|
33
|
+
Promise.resolve().then(() =>
|
|
34
|
+
logError(`Loader error: ${errorObj.message}`, {
|
|
35
|
+
"error.message": errorObj.message,
|
|
36
|
+
"error.name": errorObj.name,
|
|
37
|
+
"error.stack": errorObj.stack || "No stack trace available",
|
|
38
|
+
"error.component": componentName || "Unknown",
|
|
39
|
+
"error.type": "loader_error",
|
|
40
|
+
"loader.query": JSON.stringify(data.query),
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
|
|
28
44
|
if (onError) {
|
|
29
45
|
onError(error as Error, data.query);
|
|
30
46
|
}
|
|
@@ -38,6 +54,21 @@ export function withLoaderErrorBoundary<TQuery, TReturn>(
|
|
|
38
54
|
`[${componentName || "Loader"}] Failed to get client context:`,
|
|
39
55
|
contextError
|
|
40
56
|
);
|
|
57
|
+
|
|
58
|
+
// Log context retrieval error
|
|
59
|
+
const contextErrorObj = contextError as Error;
|
|
60
|
+
|
|
61
|
+
Promise.resolve().then(() =>
|
|
62
|
+
logError(`Failed to get client context in loader`, {
|
|
63
|
+
"error.message": contextErrorObj.message,
|
|
64
|
+
"error.name": contextErrorObj.name,
|
|
65
|
+
"error.stack":
|
|
66
|
+
contextErrorObj.stack || "No stack trace available",
|
|
67
|
+
"error.component": componentName || "Unknown",
|
|
68
|
+
"error.type": "loader_context_error",
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
41
72
|
clientContext = {
|
|
42
73
|
cookie: "",
|
|
43
74
|
customerId: "",
|