@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 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.12...HEAD
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
- [1.3.11]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.3.11
195
+
196
+ > > > > > > > main
197
+ > > > > > > > [1.3.11]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.3.11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.3.12",
3
+ "version": "1.3.13",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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) && this.props.onError) {
39
- this.props.onError(error, errorInfo);
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
+ }
@@ -10,6 +10,7 @@ export {
10
10
  validateAccessService,
11
11
  type ValidateAccessServiceProps,
12
12
  } from "./validate-access.service";
13
+ export * from "./logger";
13
14
  export {
14
15
  getDependenciesVersionService,
15
16
  type GetDependenciesVersionProps,
@@ -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,4 @@
1
+ export * from "./types";
2
+ export * from "./constants";
3
+ export * from "./context";
4
+ export * from "./otlp-logger.service";
@@ -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
+ };
@@ -13,4 +13,4 @@ export const LOCAL_STORAGE_LOCATION_EDIT_KEY = "bp_hide_edit_location_confirm";
13
13
  export const LOCAL_STORAGE_RECIPIENT_EDIT_KEY =
14
14
  "bp_hide_edit_recipient_confirm";
15
15
 
16
- export const CURRENT_VERSION = "1.3.12";
16
+ export const CURRENT_VERSION = "1.3.13";
@@ -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
- if (onError && error instanceof Error) {
36
- const clientError = error as Partial<ClientError>;
37
- const requestInfo = {
38
- url:
39
- typeof clientError.url === "string" ? clientError.url : "unknown",
40
- method:
41
- typeof clientError.method === "string"
42
- ? clientError.method
43
- : "unknown",
44
- data:
45
- clientError.responseData !== undefined
46
- ? clientError.responseData
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: "",