@lowdefy/server 0.0.0-experimental-20251203205559 → 0.0.0-experimental-20260112140412
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/lib/client/createLogUsage.js +2 -1
- package/lib/client/sentry/captureSentryError.js +43 -0
- package/lib/client/sentry/initSentryClient.js +55 -0
- package/lib/client/sentry/setSentryUser.js +41 -0
- package/lib/server/apiWrapper.js +16 -3
- package/lib/server/log/logError.js +99 -43
- package/lib/server/sentry/captureSentryError.js +57 -0
- package/lib/server/sentry/initSentry.js +52 -0
- package/lib/server/sentry/setSentryUser.js +46 -0
- package/lib/server/serverSidePropsWrapper.js +2 -2
- package/next.config.js +12 -1
- package/package.json +21 -14
- package/package.original.json +21 -14
- package/pages/_app.js +22 -1
- package/pages/api/client-error.js +47 -0
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import { v4 as uuid } from 'uuid';
|
|
16
17
|
|
|
17
18
|
// What if this fails?
|
|
18
19
|
// What about public usage
|
|
@@ -22,7 +23,7 @@ function createLogUsage({ usageDataRef }) {
|
|
|
22
23
|
let isOffline = false;
|
|
23
24
|
let machine = localStorage.getItem('lowdefy_machine_id');
|
|
24
25
|
if (!machine) {
|
|
25
|
-
machine =
|
|
26
|
+
machine = uuid();
|
|
26
27
|
localStorage.setItem('lowdefy_machine_id', machine);
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2024 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as Sentry from '@sentry/nextjs';
|
|
18
|
+
|
|
19
|
+
function captureSentryError({ error, pageId, blockId, configLocation }) {
|
|
20
|
+
const tags = {};
|
|
21
|
+
const extra = {};
|
|
22
|
+
|
|
23
|
+
// Add Lowdefy-specific context
|
|
24
|
+
if (pageId) {
|
|
25
|
+
tags.pageId = pageId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (blockId) {
|
|
29
|
+
tags.blockId = blockId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Add config location context
|
|
33
|
+
if (configLocation) {
|
|
34
|
+
extra.configLocation = configLocation;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Sentry.captureException(error, {
|
|
38
|
+
tags,
|
|
39
|
+
extra,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default captureSentryError;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2024 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as Sentry from '@sentry/nextjs';
|
|
18
|
+
|
|
19
|
+
let initialized = false;
|
|
20
|
+
|
|
21
|
+
function initSentryClient({ sentryDsn, sentryConfig }) {
|
|
22
|
+
// No-op if already initialized
|
|
23
|
+
if (initialized) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// No-op if SENTRY_DSN not set
|
|
28
|
+
if (!sentryDsn) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// No-op if client logging is explicitly disabled
|
|
33
|
+
if (sentryConfig?.client === false) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const config = sentryConfig || {};
|
|
38
|
+
|
|
39
|
+
Sentry.init({
|
|
40
|
+
dsn: sentryDsn,
|
|
41
|
+
environment: config.environment || process.env.NODE_ENV || 'production',
|
|
42
|
+
tracesSampleRate: config.tracesSampleRate ?? 0.1,
|
|
43
|
+
replaysSessionSampleRate: config.replaysSessionSampleRate ?? 0,
|
|
44
|
+
replaysOnErrorSampleRate: config.replaysOnErrorSampleRate ?? 0.1,
|
|
45
|
+
integrations: [
|
|
46
|
+
Sentry.replayIntegration(),
|
|
47
|
+
...(config.feedback ? [Sentry.feedbackIntegration({ colorScheme: 'system' })] : []),
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
initialized = true;
|
|
52
|
+
console.log('Sentry enabled: client');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default initSentryClient;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2024 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as Sentry from '@sentry/nextjs';
|
|
18
|
+
|
|
19
|
+
function setSentryUser({ user, sentryConfig }) {
|
|
20
|
+
// No-op if no user
|
|
21
|
+
if (!user) {
|
|
22
|
+
Sentry.setUser(null);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const userFields = sentryConfig?.userFields || ['id', '_id'];
|
|
27
|
+
const sentryUser = {};
|
|
28
|
+
|
|
29
|
+
userFields.forEach((field) => {
|
|
30
|
+
if (user[field] !== undefined) {
|
|
31
|
+
sentryUser[field] = user[field];
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Only set user if we have at least one field
|
|
36
|
+
if (Object.keys(sentryUser).length > 0) {
|
|
37
|
+
Sentry.setUser(sentryUser);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default setSentryUser;
|
package/lib/server/apiWrapper.js
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import path from 'path';
|
|
18
|
-
import crypto from 'crypto';
|
|
19
18
|
import { createApiContext } from '@lowdefy/api';
|
|
20
19
|
import { getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
20
|
+
import { v4 as uuid } from 'uuid';
|
|
21
21
|
|
|
22
22
|
import config from '../../build/config.json';
|
|
23
23
|
import connections from '../../build/plugins/connections.js';
|
|
@@ -29,6 +29,14 @@ import logRequest from './log/logRequest.js';
|
|
|
29
29
|
import operators from '../../build/plugins/operators/server.js';
|
|
30
30
|
import jsMap from '../../build/plugins/operators/serverJsMap.js';
|
|
31
31
|
import getAuthOptions from './auth/getAuthOptions.js';
|
|
32
|
+
import setSentryUser from './sentry/setSentryUser.js';
|
|
33
|
+
|
|
34
|
+
let loggerConfig = {};
|
|
35
|
+
try {
|
|
36
|
+
loggerConfig = require('../../build/logger.json');
|
|
37
|
+
} catch {
|
|
38
|
+
// logger.json may not exist if logger is not configured
|
|
39
|
+
}
|
|
32
40
|
|
|
33
41
|
const secrets = getSecretsFromEnv();
|
|
34
42
|
|
|
@@ -36,7 +44,7 @@ function apiWrapper(handler) {
|
|
|
36
44
|
return async function wrappedHandler(req, res) {
|
|
37
45
|
const context = {
|
|
38
46
|
// Important to give absolute path so Next can trace build files
|
|
39
|
-
rid:
|
|
47
|
+
rid: uuid(),
|
|
40
48
|
buildDirectory: path.join(process.cwd(), 'build'),
|
|
41
49
|
config,
|
|
42
50
|
connections,
|
|
@@ -54,6 +62,11 @@ function apiWrapper(handler) {
|
|
|
54
62
|
context.authOptions = getAuthOptions(context);
|
|
55
63
|
if (!req.url.startsWith('/api/auth')) {
|
|
56
64
|
context.session = await getServerSession(context);
|
|
65
|
+
// Set Sentry user context for authenticated requests
|
|
66
|
+
setSentryUser({
|
|
67
|
+
user: context.session?.user,
|
|
68
|
+
sentryConfig: loggerConfig.sentry,
|
|
69
|
+
});
|
|
57
70
|
}
|
|
58
71
|
createApiContext(context);
|
|
59
72
|
logRequest({ context });
|
|
@@ -61,7 +74,7 @@ function apiWrapper(handler) {
|
|
|
61
74
|
const response = await handler({ context, req, res });
|
|
62
75
|
return response;
|
|
63
76
|
} catch (error) {
|
|
64
|
-
logError({ error, context });
|
|
77
|
+
await logError({ error, context });
|
|
65
78
|
res.status(500).json({ name: error.name, message: error.message });
|
|
66
79
|
}
|
|
67
80
|
};
|
|
@@ -14,53 +14,109 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
import { resolveConfigLocation } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import captureSentryError from '../sentry/captureSentryError.js';
|
|
20
|
+
|
|
21
|
+
async function resolveErrorConfigLocation(context, error) {
|
|
22
|
+
if (!error.configKey) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const [keyMap, refMap] = await Promise.all([
|
|
27
|
+
context.readConfigFile('keyMap.json'),
|
|
28
|
+
context.readConfigFile('refMap.json'),
|
|
29
|
+
]);
|
|
30
|
+
const location = resolveConfigLocation({
|
|
31
|
+
configKey: error.configKey,
|
|
32
|
+
keyMap,
|
|
33
|
+
refMap,
|
|
34
|
+
configDirectory: context.configDirectory,
|
|
35
|
+
});
|
|
36
|
+
return location || null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function logError({ context, error }) {
|
|
18
43
|
try {
|
|
19
44
|
const { headers = {}, user = {} } = context;
|
|
45
|
+
const message = error?.message || 'Unknown error';
|
|
46
|
+
const isServiceError = error?.isServiceError === true;
|
|
20
47
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
// For service errors, don't resolve config location (not a config issue)
|
|
49
|
+
const location = isServiceError ? null : await resolveErrorConfigLocation(context, error);
|
|
50
|
+
|
|
51
|
+
// Human-readable console output (single log entry)
|
|
52
|
+
const errorType = isServiceError ? 'Service Error' : 'Config Error';
|
|
53
|
+
const source = location?.source ? `${location.source} at ${location.config}` : '';
|
|
54
|
+
const link = location?.link || '';
|
|
55
|
+
|
|
56
|
+
if (isServiceError) {
|
|
57
|
+
console.error(`[${errorType}] ${message}`);
|
|
58
|
+
} else {
|
|
59
|
+
console.error(`[${errorType}] ${message}\n ${source}\n ${link}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Structured logging (consistent with client error schema + production fields)
|
|
63
|
+
context.logger.error(
|
|
64
|
+
{
|
|
65
|
+
// Core error schema (consistent with client)
|
|
66
|
+
event: isServiceError ? 'service_error' : 'config_error',
|
|
67
|
+
errorName: error?.name || 'Error',
|
|
68
|
+
errorMessage: message,
|
|
69
|
+
isServiceError,
|
|
70
|
+
pageId: context.pageId || null,
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
source: location?.source || null,
|
|
73
|
+
config: location?.config || null,
|
|
74
|
+
link: location?.link || null,
|
|
75
|
+
// Production fields
|
|
76
|
+
user: {
|
|
77
|
+
id: user.id,
|
|
78
|
+
roles: user.roles,
|
|
79
|
+
sub: user.sub,
|
|
80
|
+
session_id: user.session_id,
|
|
81
|
+
},
|
|
82
|
+
url: context.req.url,
|
|
83
|
+
method: context.req.method,
|
|
84
|
+
resolvedUrl: context.nextContext?.resolvedUrl,
|
|
85
|
+
hostname: context.req.hostname,
|
|
86
|
+
headers: {
|
|
87
|
+
'accept-language': headers['accept-language'],
|
|
88
|
+
'sec-ch-ua-mobile': headers['sec-ch-ua-mobile'],
|
|
89
|
+
'sec-ch-ua-platform': headers['sec-ch-ua-platform'],
|
|
90
|
+
'sec-ch-ua': headers['sec-ch-ua'],
|
|
91
|
+
'user-agent': headers['user-agent'],
|
|
92
|
+
host: headers.host,
|
|
93
|
+
referer: headers.referer,
|
|
94
|
+
// Non localhost headers
|
|
95
|
+
'x-forward-for': headers['x-forward-for'],
|
|
96
|
+
// Vercel headers
|
|
97
|
+
'x-vercel-id': headers['x-vercel-id'],
|
|
98
|
+
'x-real-ip': headers['x-real-ip'],
|
|
99
|
+
'x-vercel-ip-country': headers['x-vercel-ip-country'],
|
|
100
|
+
'x-vercel-ip-country-region': headers['x-vercel-ip-country-region'],
|
|
101
|
+
'x-vercel-ip-city': headers['x-vercel-ip-city'],
|
|
102
|
+
'x-vercel-ip-latitude': headers['x-vercel-ip-latitude'],
|
|
103
|
+
'x-vercel-ip-longitude': headers['x-vercel-ip-longitude'],
|
|
104
|
+
'x-vercel-ip-timezone': headers['x-vercel-ip-timezone'],
|
|
105
|
+
// Cloudflare headers
|
|
106
|
+
'cf-connecting-ip': headers['cf-connecting-ip'],
|
|
107
|
+
'cf-ray': headers['cf-ray'],
|
|
108
|
+
'cf-ipcountry': headers['cf-ipcountry'],
|
|
109
|
+
'cf-visitor': headers['cf-visitor'],
|
|
110
|
+
},
|
|
63
111
|
},
|
|
112
|
+
message
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Capture error to Sentry (no-op if Sentry not configured)
|
|
116
|
+
captureSentryError({
|
|
117
|
+
error,
|
|
118
|
+
context,
|
|
119
|
+
configLocation: location,
|
|
64
120
|
});
|
|
65
121
|
} catch (e) {
|
|
66
122
|
console.error(error);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2024 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as Sentry from '@sentry/nextjs';
|
|
18
|
+
|
|
19
|
+
function captureSentryError({ error, context, configLocation }) {
|
|
20
|
+
// No-op if Sentry not initialized (DSN not set)
|
|
21
|
+
if (!process.env.SENTRY_DSN) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tags = {};
|
|
26
|
+
const extra = {};
|
|
27
|
+
|
|
28
|
+
// Add Lowdefy-specific context
|
|
29
|
+
if (context?.pageId) {
|
|
30
|
+
tags.pageId = context.pageId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (error?.blockId) {
|
|
34
|
+
tags.blockId = error.blockId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (error?.isServiceError !== undefined) {
|
|
38
|
+
tags.isServiceError = error.isServiceError;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Add config location context
|
|
42
|
+
if (configLocation) {
|
|
43
|
+
extra.configLocation = configLocation;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add config key for reference
|
|
47
|
+
if (error?.configKey) {
|
|
48
|
+
extra.configKey = error.configKey;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Sentry.captureException(error, {
|
|
52
|
+
tags,
|
|
53
|
+
extra,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default captureSentryError;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2024 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as Sentry from '@sentry/nextjs';
|
|
18
|
+
|
|
19
|
+
function loadLoggerConfig() {
|
|
20
|
+
try {
|
|
21
|
+
// Dynamic require to handle missing file gracefully
|
|
22
|
+
return require('../../../build/logger.json');
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function initSentryServer() {
|
|
29
|
+
// No-op if SENTRY_DSN not set
|
|
30
|
+
if (!process.env.SENTRY_DSN) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const loggerConfig = loadLoggerConfig();
|
|
35
|
+
const sentryConfig = loggerConfig.sentry || {};
|
|
36
|
+
|
|
37
|
+
// No-op if server logging is explicitly disabled
|
|
38
|
+
if (sentryConfig.server === false) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Sentry.init({
|
|
43
|
+
dsn: process.env.SENTRY_DSN,
|
|
44
|
+
environment: sentryConfig.environment || process.env.NODE_ENV || 'production',
|
|
45
|
+
tracesSampleRate: sentryConfig.tracesSampleRate ?? 0.1,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log('Sentry enabled: server');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default initSentryServer;
|
|
52
|
+
export { initSentryServer };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2024 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as Sentry from '@sentry/nextjs';
|
|
18
|
+
|
|
19
|
+
function setSentryUser({ user, sentryConfig }) {
|
|
20
|
+
// No-op if Sentry not initialized (DSN not set)
|
|
21
|
+
if (!process.env.SENTRY_DSN) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// No-op if no user
|
|
26
|
+
if (!user) {
|
|
27
|
+
Sentry.setUser(null);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const userFields = sentryConfig?.userFields || ['id', '_id'];
|
|
32
|
+
const sentryUser = {};
|
|
33
|
+
|
|
34
|
+
userFields.forEach((field) => {
|
|
35
|
+
if (user[field] !== undefined) {
|
|
36
|
+
sentryUser[field] = user[field];
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Only set user if we have at least one field
|
|
41
|
+
if (Object.keys(sentryUser).length > 0) {
|
|
42
|
+
Sentry.setUser(sentryUser);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default setSentryUser;
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import path from 'path';
|
|
18
|
-
import crypto from 'crypto';
|
|
19
18
|
import { createApiContext } from '@lowdefy/api';
|
|
19
|
+
import { v4 as uuid } from 'uuid';
|
|
20
20
|
|
|
21
21
|
import config from '../../build/config.json';
|
|
22
22
|
import createLogger from './log/createLogger.js';
|
|
@@ -31,7 +31,7 @@ function serverSidePropsWrapper(handler) {
|
|
|
31
31
|
return async function wrappedHandler(nextContext) {
|
|
32
32
|
const context = {
|
|
33
33
|
// Important to give absolute path so Next can trace build files
|
|
34
|
-
rid:
|
|
34
|
+
rid: uuid(),
|
|
35
35
|
buildDirectory: path.join(process.cwd(), 'build'),
|
|
36
36
|
config,
|
|
37
37
|
fileCache,
|
package/next.config.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
const { withSentryConfig } = require('@sentry/nextjs');
|
|
1
2
|
const withLess = require('next-with-less');
|
|
2
3
|
const lowdefyConfig = require('./build/config.json');
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
const nextConfig = withLess({
|
|
5
6
|
basePath: lowdefyConfig.basePath,
|
|
6
7
|
reactStrictMode: true,
|
|
7
8
|
webpack: (config, { isServer }) => {
|
|
@@ -27,3 +28,13 @@ module.exports = withLess({
|
|
|
27
28
|
ignoreDuringBuilds: true,
|
|
28
29
|
},
|
|
29
30
|
});
|
|
31
|
+
|
|
32
|
+
// Only wrap with Sentry if SENTRY_DSN is configured
|
|
33
|
+
// This enables source map uploads when SENTRY_AUTH_TOKEN is present
|
|
34
|
+
module.exports = process.env.SENTRY_DSN
|
|
35
|
+
? withSentryConfig(nextConfig, {
|
|
36
|
+
// Sentry options
|
|
37
|
+
silent: true,
|
|
38
|
+
hideSourceMaps: true,
|
|
39
|
+
})
|
|
40
|
+
: nextConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/server",
|
|
3
|
-
"version": "0.0.0-experimental-
|
|
3
|
+
"version": "0.0.0-experimental-20260112140412",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -36,18 +36,25 @@
|
|
|
36
36
|
".npmrc"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@lowdefy/actions-core": "0.0.0-experimental-
|
|
40
|
-
"@lowdefy/api": "0.0.0-experimental-
|
|
41
|
-
"@lowdefy/block-utils": "0.0.0-experimental-
|
|
42
|
-
"@lowdefy/blocks-antd": "0.0.0-experimental-
|
|
43
|
-
"@lowdefy/blocks-basic": "0.0.0-experimental-
|
|
44
|
-
"@lowdefy/blocks-loaders": "0.0.0-experimental-
|
|
45
|
-
"@lowdefy/
|
|
46
|
-
"@lowdefy/
|
|
47
|
-
"@lowdefy/
|
|
48
|
-
"@lowdefy/
|
|
49
|
-
"@lowdefy/
|
|
50
|
-
"@lowdefy/
|
|
39
|
+
"@lowdefy/actions-core": "0.0.0-experimental-20260112140412",
|
|
40
|
+
"@lowdefy/api": "0.0.0-experimental-20260112140412",
|
|
41
|
+
"@lowdefy/block-utils": "0.0.0-experimental-20260112140412",
|
|
42
|
+
"@lowdefy/blocks-antd": "0.0.0-experimental-20260112140412",
|
|
43
|
+
"@lowdefy/blocks-basic": "0.0.0-experimental-20260112140412",
|
|
44
|
+
"@lowdefy/blocks-loaders": "0.0.0-experimental-20260112140412",
|
|
45
|
+
"@lowdefy/blocks-markdown": "0.0.0-experimental-20260112140412",
|
|
46
|
+
"@lowdefy/client": "0.0.0-experimental-20260112140412",
|
|
47
|
+
"@lowdefy/connection-axios-http": "0.0.0-experimental-20260112140412",
|
|
48
|
+
"@lowdefy/connection-mongodb": "0.0.0-experimental-20260112140412",
|
|
49
|
+
"@lowdefy/helpers": "0.0.0-experimental-20260112140412",
|
|
50
|
+
"@lowdefy/layout": "0.0.0-experimental-20260112140412",
|
|
51
|
+
"@lowdefy/node-utils": "0.0.0-experimental-20260112140412",
|
|
52
|
+
"@lowdefy/operators-js": "0.0.0-experimental-20260112140412",
|
|
53
|
+
"@lowdefy/operators-nunjucks": "0.0.0-experimental-20260112140412",
|
|
54
|
+
"@lowdefy/operators-uuid": "0.0.0-experimental-20260112140412",
|
|
55
|
+
"@lowdefy/plugin-next-auth": "0.0.0-experimental-20260112140412",
|
|
56
|
+
"@sentry/nextjs": "8.53.0",
|
|
57
|
+
"uuid": "13.0.0",
|
|
51
58
|
"next": "13.5.4",
|
|
52
59
|
"next-auth": "4.24.5",
|
|
53
60
|
"pino": "8.16.2",
|
|
@@ -57,7 +64,7 @@
|
|
|
57
64
|
"react-icons": "4.12.0"
|
|
58
65
|
},
|
|
59
66
|
"devDependencies": {
|
|
60
|
-
"@lowdefy/build": "0.0.0-experimental-
|
|
67
|
+
"@lowdefy/build": "0.0.0-experimental-20260112140412",
|
|
61
68
|
"@next/eslint-plugin-next": "13.5.4",
|
|
62
69
|
"less": "4.2.0",
|
|
63
70
|
"less-loader": "11.1.3",
|
package/package.original.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/server",
|
|
3
|
-
"version": "0.0.0-experimental-
|
|
3
|
+
"version": "0.0.0-experimental-20260112140412",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -46,18 +46,25 @@
|
|
|
46
46
|
"prepublishOnly": "pnpm build"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lowdefy/actions-core": "0.0.0-experimental-
|
|
50
|
-
"@lowdefy/api": "0.0.0-experimental-
|
|
51
|
-
"@lowdefy/block-utils": "0.0.0-experimental-
|
|
52
|
-
"@lowdefy/blocks-antd": "0.0.0-experimental-
|
|
53
|
-
"@lowdefy/blocks-basic": "0.0.0-experimental-
|
|
54
|
-
"@lowdefy/blocks-loaders": "0.0.0-experimental-
|
|
55
|
-
"@lowdefy/
|
|
56
|
-
"@lowdefy/
|
|
57
|
-
"@lowdefy/
|
|
58
|
-
"@lowdefy/
|
|
59
|
-
"@lowdefy/
|
|
60
|
-
"@lowdefy/
|
|
49
|
+
"@lowdefy/actions-core": "0.0.0-experimental-20260112140412",
|
|
50
|
+
"@lowdefy/api": "0.0.0-experimental-20260112140412",
|
|
51
|
+
"@lowdefy/block-utils": "0.0.0-experimental-20260112140412",
|
|
52
|
+
"@lowdefy/blocks-antd": "0.0.0-experimental-20260112140412",
|
|
53
|
+
"@lowdefy/blocks-basic": "0.0.0-experimental-20260112140412",
|
|
54
|
+
"@lowdefy/blocks-loaders": "0.0.0-experimental-20260112140412",
|
|
55
|
+
"@lowdefy/blocks-markdown": "0.0.0-experimental-20260112140412",
|
|
56
|
+
"@lowdefy/client": "0.0.0-experimental-20260112140412",
|
|
57
|
+
"@lowdefy/connection-axios-http": "0.0.0-experimental-20260112140412",
|
|
58
|
+
"@lowdefy/connection-mongodb": "0.0.0-experimental-20260112140412",
|
|
59
|
+
"@lowdefy/helpers": "0.0.0-experimental-20260112140412",
|
|
60
|
+
"@lowdefy/layout": "0.0.0-experimental-20260112140412",
|
|
61
|
+
"@lowdefy/node-utils": "0.0.0-experimental-20260112140412",
|
|
62
|
+
"@lowdefy/operators-js": "0.0.0-experimental-20260112140412",
|
|
63
|
+
"@lowdefy/operators-nunjucks": "0.0.0-experimental-20260112140412",
|
|
64
|
+
"@lowdefy/operators-uuid": "0.0.0-experimental-20260112140412",
|
|
65
|
+
"@lowdefy/plugin-next-auth": "0.0.0-experimental-20260112140412",
|
|
66
|
+
"@sentry/nextjs": "8.53.0",
|
|
67
|
+
"uuid": "13.0.0",
|
|
61
68
|
"next": "13.5.4",
|
|
62
69
|
"next-auth": "4.24.5",
|
|
63
70
|
"pino": "8.16.2",
|
|
@@ -67,7 +74,7 @@
|
|
|
67
74
|
"react-icons": "4.12.0"
|
|
68
75
|
},
|
|
69
76
|
"devDependencies": {
|
|
70
|
-
"@lowdefy/build": "0.0.0-experimental-
|
|
77
|
+
"@lowdefy/build": "0.0.0-experimental-20260112140412",
|
|
71
78
|
"@next/eslint-plugin-next": "13.5.4",
|
|
72
79
|
"less": "4.2.0",
|
|
73
80
|
"less-loader": "11.1.3",
|
package/pages/_app.js
CHANGED
|
@@ -14,25 +14,46 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React, { useRef } from 'react';
|
|
17
|
+
import React, { useEffect, useRef } from 'react';
|
|
18
18
|
import dynamic from 'next/dynamic';
|
|
19
19
|
|
|
20
20
|
import { ErrorBoundary } from '@lowdefy/block-utils';
|
|
21
21
|
|
|
22
22
|
import Auth from '../lib/client/auth/Auth.js';
|
|
23
23
|
import createLogUsage from '../lib/client/createLogUsage.js';
|
|
24
|
+
import initSentryClient from '../lib/client/sentry/initSentryClient.js';
|
|
25
|
+
import setSentryUser from '../lib/client/sentry/setSentryUser.js';
|
|
26
|
+
|
|
27
|
+
let loggerConfig = {};
|
|
28
|
+
try {
|
|
29
|
+
loggerConfig = require('../build/logger.json');
|
|
30
|
+
} catch {
|
|
31
|
+
// logger.json may not exist if Sentry is not configured
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
// Must be in _app due to next specifications.
|
|
26
35
|
import '../build/plugins/styles.less';
|
|
27
36
|
|
|
37
|
+
// Initialize Sentry client once on module load
|
|
38
|
+
initSentryClient({
|
|
39
|
+
sentryDsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
40
|
+
sentryConfig: loggerConfig.sentry,
|
|
41
|
+
});
|
|
42
|
+
|
|
28
43
|
function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
|
|
29
44
|
const usageDataRef = useRef({});
|
|
30
45
|
const lowdefyRef = useRef({ eventCallback: createLogUsage({ usageDataRef }) });
|
|
46
|
+
|
|
31
47
|
return (
|
|
32
48
|
<ErrorBoundary fullPage>
|
|
33
49
|
<Auth session={session}>
|
|
34
50
|
{(auth) => {
|
|
35
51
|
usageDataRef.current.user = auth.session?.hashed_id;
|
|
52
|
+
// Set Sentry user context when auth changes
|
|
53
|
+
setSentryUser({
|
|
54
|
+
user: auth.session,
|
|
55
|
+
sentryConfig: loggerConfig.sentry,
|
|
56
|
+
});
|
|
36
57
|
return (
|
|
37
58
|
<Component
|
|
38
59
|
auth={auth}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2024 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { logClientError } from '@lowdefy/api';
|
|
18
|
+
|
|
19
|
+
import apiWrapper from '../../lib/server/apiWrapper.js';
|
|
20
|
+
import captureSentryError from '../../lib/server/sentry/captureSentryError.js';
|
|
21
|
+
|
|
22
|
+
async function handler({ context, req, res }) {
|
|
23
|
+
if (req.method !== 'POST') {
|
|
24
|
+
throw new Error('Only POST requests are supported.');
|
|
25
|
+
}
|
|
26
|
+
const { configKey, message, name, pageId, timestamp } = req.body;
|
|
27
|
+
const response = await logClientError(context, {
|
|
28
|
+
configKey,
|
|
29
|
+
message,
|
|
30
|
+
name,
|
|
31
|
+
pageId,
|
|
32
|
+
timestamp,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Capture client error to Sentry (no-op if Sentry not configured)
|
|
36
|
+
captureSentryError({
|
|
37
|
+
error: new Error(message),
|
|
38
|
+
context: { ...context, pageId },
|
|
39
|
+
configLocation: response.source
|
|
40
|
+
? { source: response.source, config: response.config, link: response.link }
|
|
41
|
+
: null,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
res.status(200).json(response);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default apiWrapper(handler);
|