@lowdefy/server-dev 0.0.0-experimental-20260220143146 → 0.0.0-experimental-20260220145512
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/BuildErrorPage.js +3 -4
- package/lib/client/Page.js +0 -4
- package/lib/client/RestartingPage.js +0 -2
- 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/client/utils/request.js +3 -1
- package/lib/client/utils/usePageConfig.js +0 -3
- package/lib/server/apiWrapper.js +10 -14
- package/lib/server/auth/getMockSession.js +1 -1
- package/lib/server/jitPageBuilder.js +1 -17
- package/lib/server/log/createLogger.js +2 -2
- package/lib/server/log/logError.js +56 -0
- package/lib/server/pageCache.mjs +13 -0
- package/lib/server/pageCache.test.mjs +39 -0
- package/lib/server/sentry/captureSentryError.js +57 -0
- package/lib/server/sentry/initSentry.js +44 -0
- package/lib/server/sentry/setSentryUser.js +46 -0
- package/manager/getContext.mjs +4 -8
- package/manager/processes/installPlugins.mjs +2 -2
- package/manager/processes/lowdefyBuild.mjs +2 -2
- package/manager/processes/nextBuild.mjs +6 -4
- package/manager/processes/restartServer.mjs +2 -2
- package/manager/processes/shutdownServer.mjs +1 -1
- package/manager/utils/checkPortAvailable.mjs +4 -5
- package/manager/utils/getLowdefyVersion.mjs +12 -6
- package/manager/watchers/lowdefyBuildWatcher.mjs +24 -4
- package/manager/watchers/nextBuildWatcher.mjs +2 -12
- package/next.config.js +10 -1
- package/package.json +30 -30
- package/package.original.json +30 -30
- package/pages/_app.js +17 -3
- package/pages/api/client-error.js +10 -15
- package/pages/api/page/[pageId].js +3 -11
- package/pages/api/request/[pageId]/[requestId].js +1 -4
- package/lib/client/InstallingPluginsPage.js +0 -50
- package/lib/server/log/createHandleError.js +0 -47
|
@@ -21,7 +21,7 @@ const typeColors = {
|
|
|
21
21
|
ConfigWarning: '#d48806',
|
|
22
22
|
PluginError: '#531dab',
|
|
23
23
|
ServiceError: '#096dd9',
|
|
24
|
-
|
|
24
|
+
LowdefyError: '#cf1322',
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
function getTypeColor(type) {
|
|
@@ -49,7 +49,7 @@ const ErrorItem = ({ type, message, source }) => {
|
|
|
49
49
|
>
|
|
50
50
|
{type}
|
|
51
51
|
</span>
|
|
52
|
-
<p style={{ fontSize: 14, margin: '4px 0'
|
|
52
|
+
<p style={{ fontSize: 14, margin: '4px 0' }}>{message}</p>
|
|
53
53
|
{source && <p style={{ fontSize: 13, color: '#8c8c8c', margin: 0 }}>{source}</p>}
|
|
54
54
|
</div>
|
|
55
55
|
);
|
|
@@ -67,8 +67,7 @@ const BuildErrorPage = ({ errors, message, source }) => {
|
|
|
67
67
|
flexDirection: 'column',
|
|
68
68
|
alignItems: 'center',
|
|
69
69
|
justifyContent: 'center',
|
|
70
|
-
fontFamily:
|
|
71
|
-
"system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
70
|
+
fontFamily: 'monospace',
|
|
72
71
|
padding: '0 24px',
|
|
73
72
|
}}
|
|
74
73
|
>
|
package/lib/client/Page.js
CHANGED
|
@@ -18,7 +18,6 @@ import React from 'react';
|
|
|
18
18
|
import Client from '@lowdefy/client';
|
|
19
19
|
|
|
20
20
|
import BuildErrorPage from './BuildErrorPage.js';
|
|
21
|
-
import InstallingPluginsPage from './InstallingPluginsPage.js';
|
|
22
21
|
import RestartingPage from './RestartingPage.js';
|
|
23
22
|
import usePageConfig from './utils/usePageConfig.js';
|
|
24
23
|
|
|
@@ -48,9 +47,6 @@ const Page = ({
|
|
|
48
47
|
/>
|
|
49
48
|
);
|
|
50
49
|
}
|
|
51
|
-
if (pageConfig.installing) {
|
|
52
|
-
return <InstallingPluginsPage packages={pageConfig.packages} />;
|
|
53
|
-
}
|
|
54
50
|
if (resetContext.restarting) {
|
|
55
51
|
return <RestartingPage />;
|
|
56
52
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 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-2026 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-2026 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;
|
|
@@ -27,7 +27,9 @@ async function request({ url, method = 'GET', body }) {
|
|
|
27
27
|
}
|
|
28
28
|
if (!res.ok) {
|
|
29
29
|
const body = await res.json();
|
|
30
|
-
|
|
30
|
+
console.log(res);
|
|
31
|
+
console.log(body);
|
|
32
|
+
throw new Error(body.message || 'Request error');
|
|
31
33
|
}
|
|
32
34
|
return res.json();
|
|
33
35
|
}
|
package/lib/server/apiWrapper.js
CHANGED
|
@@ -17,7 +17,6 @@ import fs from 'fs';
|
|
|
17
17
|
import path from 'path';
|
|
18
18
|
import { createApiContext } from '@lowdefy/api';
|
|
19
19
|
import { getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
20
|
-
import { serializer } from '@lowdefy/helpers';
|
|
21
20
|
import { v4 as uuid } from 'uuid';
|
|
22
21
|
|
|
23
22
|
import config from '../build/config.js';
|
|
@@ -25,11 +24,13 @@ import connections from '../../build/plugins/connections.js';
|
|
|
25
24
|
import createLogger from './log/createLogger.js';
|
|
26
25
|
import fileCache from './fileCache.js';
|
|
27
26
|
import getServerSession from './auth/getServerSession.js';
|
|
28
|
-
import
|
|
27
|
+
import logError from './log/logError.js';
|
|
29
28
|
import logRequest from './log/logRequest.js';
|
|
30
29
|
import operators from '../../build/plugins/operators/server.js';
|
|
31
30
|
import staticJsMap from '../../build/plugins/operators/serverJsMap.js';
|
|
32
31
|
import getAuthOptions from './auth/getAuthOptions.js';
|
|
32
|
+
import loggerConfig from '../build/logger.js';
|
|
33
|
+
import setSentryUser from './sentry/setSentryUser.js';
|
|
33
34
|
|
|
34
35
|
const secrets = getSecretsFromEnv();
|
|
35
36
|
|
|
@@ -72,9 +73,6 @@ function apiWrapper(handler) {
|
|
|
72
73
|
fileCache,
|
|
73
74
|
headers: req?.headers,
|
|
74
75
|
jsMap,
|
|
75
|
-
handleError: async (err) => {
|
|
76
|
-
console.error(err);
|
|
77
|
-
},
|
|
78
76
|
logger: console,
|
|
79
77
|
operators,
|
|
80
78
|
req,
|
|
@@ -83,10 +81,14 @@ function apiWrapper(handler) {
|
|
|
83
81
|
};
|
|
84
82
|
try {
|
|
85
83
|
context.logger = createLogger({ rid: context.rid });
|
|
86
|
-
context.handleError = createHandleError({ context });
|
|
87
84
|
context.authOptions = getAuthOptions(context);
|
|
88
85
|
if (!req.url.startsWith('/api/auth')) {
|
|
89
86
|
context.session = await getServerSession(context);
|
|
87
|
+
// Set Sentry user context for authenticated requests
|
|
88
|
+
setSentryUser({
|
|
89
|
+
user: context.session?.user,
|
|
90
|
+
sentryConfig: loggerConfig.sentry,
|
|
91
|
+
});
|
|
90
92
|
}
|
|
91
93
|
createApiContext(context);
|
|
92
94
|
logRequest({ context });
|
|
@@ -95,14 +97,8 @@ function apiWrapper(handler) {
|
|
|
95
97
|
// TODO: Log response time?
|
|
96
98
|
return response;
|
|
97
99
|
} catch (error) {
|
|
98
|
-
await
|
|
99
|
-
|
|
100
|
-
if (serialized?.['~e']) {
|
|
101
|
-
delete serialized['~e'].received;
|
|
102
|
-
delete serialized['~e'].stack;
|
|
103
|
-
delete serialized['~e'].configKey;
|
|
104
|
-
}
|
|
105
|
-
res.status(500).json(serialized);
|
|
100
|
+
await logError({ error, context });
|
|
101
|
+
res.status(500).json({ name: error.name, message: error.message });
|
|
106
102
|
}
|
|
107
103
|
};
|
|
108
104
|
}
|
|
@@ -28,7 +28,7 @@ async function getMockSession() {
|
|
|
28
28
|
try {
|
|
29
29
|
mockUser = JSON.parse(mockUserJson);
|
|
30
30
|
} catch (error) {
|
|
31
|
-
throw new Error(
|
|
31
|
+
throw new Error(`Invalid JSON in LOWDEFY_DEV_USER environment variable: ${error.message}`);
|
|
32
32
|
}
|
|
33
33
|
} else {
|
|
34
34
|
mockUser = authJson.dev?.mockUser;
|
|
@@ -85,14 +85,10 @@ function getBuildContext(buildDirectory, configDirectory) {
|
|
|
85
85
|
const jsMap = readJsonFile(path.join(buildDirectory, 'jsMap.json')) ?? { client: {}, server: {} };
|
|
86
86
|
const connectionIds = readJsonFile(path.join(buildDirectory, 'connectionIds.json')) ?? [];
|
|
87
87
|
|
|
88
|
-
const customTypesMap = readJsonFile(path.join(buildDirectory, 'customTypesMap.json')) ?? {};
|
|
89
|
-
|
|
90
88
|
cachedBuildContext = createContext({
|
|
91
|
-
customTypesMap,
|
|
92
89
|
directories: {
|
|
93
90
|
build: buildDirectory,
|
|
94
91
|
config: configDirectory,
|
|
95
|
-
server: path.resolve(buildDirectory, '..'),
|
|
96
92
|
},
|
|
97
93
|
logger: jitLogger,
|
|
98
94
|
stage: 'dev',
|
|
@@ -107,11 +103,6 @@ function getBuildContext(buildDirectory, configDirectory) {
|
|
|
107
103
|
cachedBuildContext.connectionIds.add(id);
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
// Load installed packages snapshot from skeleton build for missing-package detection
|
|
111
|
-
const installedPluginPackages =
|
|
112
|
-
readJsonFile(path.join(buildDirectory, 'installedPluginPackages.json')) ?? [];
|
|
113
|
-
cachedBuildContext.installedPluginPackages = new Set(installedPluginPackages);
|
|
114
|
-
|
|
115
106
|
return cachedBuildContext;
|
|
116
107
|
}
|
|
117
108
|
|
|
@@ -135,18 +126,11 @@ async function buildPageIfNeeded({ pageId, buildDirectory, configDirectory }) {
|
|
|
135
126
|
try {
|
|
136
127
|
const context = getBuildContext(buildDirectory, configDirectory);
|
|
137
128
|
const startTime = Date.now();
|
|
138
|
-
|
|
129
|
+
await buildPageJit({
|
|
139
130
|
pageId,
|
|
140
131
|
pageRegistry: registry,
|
|
141
132
|
context,
|
|
142
133
|
});
|
|
143
|
-
if (result && result.installing) {
|
|
144
|
-
jitLogger.info(
|
|
145
|
-
`Installing plugin packages for page "${pageId}": ${result.packages.join(', ')}. ` +
|
|
146
|
-
'The page will be available after the server restarts.'
|
|
147
|
-
);
|
|
148
|
-
return result;
|
|
149
|
-
}
|
|
150
134
|
pageCache.markCompiled(pageId);
|
|
151
135
|
jitLogger.info(`Built page "${pageId}" in ${Date.now() - startTime}ms.`);
|
|
152
136
|
return true;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { createNodeLogger } from '@lowdefy/logger/node';
|
|
17
|
+
import { createNodeLogger, wrapErrorLogger } from '@lowdefy/logger/node';
|
|
18
18
|
|
|
19
19
|
const logger = createNodeLogger({
|
|
20
20
|
name: 'lowdefy_server',
|
|
@@ -23,7 +23,7 @@ const logger = createNodeLogger({
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
function createLogger(metadata = {}) {
|
|
26
|
-
return logger.child(metadata);
|
|
26
|
+
return wrapErrorLogger(logger.child(metadata));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export default createLogger;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 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 { resolveErrorConfigLocation } from '@lowdefy/errors/build';
|
|
18
|
+
|
|
19
|
+
import captureSentryError from '../sentry/captureSentryError.js';
|
|
20
|
+
|
|
21
|
+
async function logError({ context, error }) {
|
|
22
|
+
try {
|
|
23
|
+
const isServiceError = error?.isServiceError === true;
|
|
24
|
+
|
|
25
|
+
// For service errors, don't resolve config location (not a config issue)
|
|
26
|
+
const location = isServiceError
|
|
27
|
+
? null
|
|
28
|
+
: await resolveErrorConfigLocation({
|
|
29
|
+
error,
|
|
30
|
+
readConfigFile: context.readConfigFile,
|
|
31
|
+
configDirectory: context.configDirectory,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Attach resolved location to error for display layer
|
|
35
|
+
if (location) {
|
|
36
|
+
error.source = location.source;
|
|
37
|
+
error.config = location.config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Log error - logger handles source, name prefix, and received formatting
|
|
41
|
+
context.logger.error(error);
|
|
42
|
+
|
|
43
|
+
// Capture error to Sentry (no-op if Sentry not configured)
|
|
44
|
+
captureSentryError({
|
|
45
|
+
error,
|
|
46
|
+
context,
|
|
47
|
+
configLocation: location,
|
|
48
|
+
});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(error);
|
|
51
|
+
console.error('An error occurred while logging the error.');
|
|
52
|
+
console.error(e);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default logError;
|
package/lib/server/pageCache.mjs
CHANGED
|
@@ -86,6 +86,19 @@ class PageCache {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
invalidateByFiles(changedFiles, fileDependencyMap) {
|
|
90
|
+
const affectedPages = new Set();
|
|
91
|
+
for (const filePath of changedFiles) {
|
|
92
|
+
const pageIds = fileDependencyMap.get(filePath);
|
|
93
|
+
if (pageIds) {
|
|
94
|
+
for (const pageId of pageIds) {
|
|
95
|
+
affectedPages.add(pageId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.invalidatePages(affectedPages);
|
|
100
|
+
return affectedPages;
|
|
101
|
+
}
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
export default PageCache;
|
|
@@ -47,6 +47,45 @@ test('invalidatePages removes specific pages', () => {
|
|
|
47
47
|
expect(cache.isCompiled('settings')).toBe(false);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test('invalidateByFiles returns affected pages and invalidates them', () => {
|
|
51
|
+
const cache = new PageCache();
|
|
52
|
+
cache.markCompiled('home');
|
|
53
|
+
cache.markCompiled('dashboard');
|
|
54
|
+
|
|
55
|
+
const fileDependencyMap = new Map([
|
|
56
|
+
['pages/home/blocks.yaml', new Set(['home'])],
|
|
57
|
+
['shared/layout.yaml', new Set(['home', 'dashboard'])],
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const affected = cache.invalidateByFiles(['pages/home/blocks.yaml'], fileDependencyMap);
|
|
61
|
+
expect(affected).toEqual(new Set(['home']));
|
|
62
|
+
expect(cache.isCompiled('home')).toBe(false);
|
|
63
|
+
expect(cache.isCompiled('dashboard')).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('invalidateByFiles handles shared files affecting multiple pages', () => {
|
|
67
|
+
const cache = new PageCache();
|
|
68
|
+
cache.markCompiled('home');
|
|
69
|
+
cache.markCompiled('dashboard');
|
|
70
|
+
|
|
71
|
+
const fileDependencyMap = new Map([['shared/layout.yaml', new Set(['home', 'dashboard'])]]);
|
|
72
|
+
|
|
73
|
+
const affected = cache.invalidateByFiles(['shared/layout.yaml'], fileDependencyMap);
|
|
74
|
+
expect(affected).toEqual(new Set(['home', 'dashboard']));
|
|
75
|
+
expect(cache.isCompiled('home')).toBe(false);
|
|
76
|
+
expect(cache.isCompiled('dashboard')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('invalidateByFiles returns empty set for unknown files', () => {
|
|
80
|
+
const cache = new PageCache();
|
|
81
|
+
cache.markCompiled('home');
|
|
82
|
+
|
|
83
|
+
const fileDependencyMap = new Map();
|
|
84
|
+
const affected = cache.invalidateByFiles(['unknown.yaml'], fileDependencyMap);
|
|
85
|
+
expect(affected).toEqual(new Set());
|
|
86
|
+
expect(cache.isCompiled('home')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
50
89
|
test('acquireBuildLock returns true for first request', async () => {
|
|
51
90
|
const cache = new PageCache();
|
|
52
91
|
const shouldBuild = await cache.acquireBuildLock('home');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 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,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 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
|
+
import loggerConfig from '../../build/logger.js';
|
|
20
|
+
|
|
21
|
+
function initSentryServer() {
|
|
22
|
+
// No-op if SENTRY_DSN not set
|
|
23
|
+
if (!process.env.SENTRY_DSN) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sentryConfig = loggerConfig.sentry || {};
|
|
28
|
+
|
|
29
|
+
// No-op if server logging is explicitly disabled
|
|
30
|
+
if (sentryConfig.server === false) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Sentry.init({
|
|
35
|
+
dsn: process.env.SENTRY_DSN,
|
|
36
|
+
environment: sentryConfig.environment || process.env.NODE_ENV || 'production',
|
|
37
|
+
tracesSampleRate: sentryConfig.tracesSampleRate ?? 0.1,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log('Sentry enabled: server');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default initSentryServer;
|
|
44
|
+
export { initSentryServer };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 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;
|
package/manager/getContext.mjs
CHANGED
|
@@ -19,8 +19,7 @@ import path from 'path';
|
|
|
19
19
|
import yargs from 'yargs';
|
|
20
20
|
import { hideBin } from 'yargs/helpers';
|
|
21
21
|
|
|
22
|
-
import
|
|
23
|
-
import { createNodeLogger } from '@lowdefy/logger/node';
|
|
22
|
+
import { createDevLogger as createLogger } from '@lowdefy/logger/dev';
|
|
24
23
|
import checkMockUserWarning from './processes/checkMockUserWarning.mjs';
|
|
25
24
|
import initialBuild from './processes/initialBuild.mjs';
|
|
26
25
|
import installPlugins from './processes/installPlugins.mjs';
|
|
@@ -49,12 +48,7 @@ async function getContext() {
|
|
|
49
48
|
config: path.resolve(argv.configDirectory ?? env.LOWDEFY_DIRECTORY_CONFIG ?? process.cwd()),
|
|
50
49
|
server: process.cwd(),
|
|
51
50
|
},
|
|
52
|
-
logger:
|
|
53
|
-
name: 'lowdefy build',
|
|
54
|
-
level: env.LOWDEFY_LOG_LEVEL ?? 'info',
|
|
55
|
-
base: { pid: undefined, hostname: undefined },
|
|
56
|
-
destination: pino.destination({ dest: 1, sync: true }),
|
|
57
|
-
}),
|
|
51
|
+
logger: createLogger({ level: env.LOWDEFY_LOG_LEVEL }),
|
|
58
52
|
options: {
|
|
59
53
|
port: argv.port ?? env.PORT ?? 3000,
|
|
60
54
|
refResolver: argv.refResolver ?? env.LOWDEFY_BUILD_REF_RESOLVER,
|
|
@@ -70,6 +64,7 @@ async function getContext() {
|
|
|
70
64
|
// JIT build state
|
|
71
65
|
pageCache: new PageCache(),
|
|
72
66
|
pageRegistry: null,
|
|
67
|
+
fileDependencyMap: null,
|
|
73
68
|
buildContext: null,
|
|
74
69
|
};
|
|
75
70
|
|
|
@@ -84,6 +79,7 @@ async function getContext() {
|
|
|
84
79
|
const result = await buildFn();
|
|
85
80
|
if (result) {
|
|
86
81
|
context.pageRegistry = result.pageRegistry;
|
|
82
|
+
context.fileDependencyMap = result.fileDependencyMap;
|
|
87
83
|
context.buildContext = result.context;
|
|
88
84
|
}
|
|
89
85
|
};
|