@lowdefy/server-dev 0.0.0-experimental-20251203205559 → 0.0.0-experimental-20260113081624

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.
@@ -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;
@@ -26,7 +26,6 @@ async function request({ url, method = 'GET', body }) {
26
26
  return null;
27
27
  }
28
28
  if (!res.ok) {
29
- // TODO: check
30
29
  const body = await res.json();
31
30
  console.log(res);
32
31
  console.log(body);
@@ -14,9 +14,9 @@
14
14
  limitations under the License.
15
15
  */
16
16
  import path from 'path';
17
- import crypto from 'crypto';
18
17
  import { createApiContext } from '@lowdefy/api';
19
18
  import { getSecretsFromEnv } from '@lowdefy/node-utils';
19
+ import { v4 as uuid } from 'uuid';
20
20
 
21
21
  import config from '../../build/config.json';
22
22
  import connections from '../../build/plugins/connections.js';
@@ -28,6 +28,14 @@ import logRequest from './log/logRequest.js';
28
28
  import operators from '../../build/plugins/operators/server.js';
29
29
  import jsMap from '../../build/plugins/operators/serverJsMap.js';
30
30
  import getAuthOptions from './auth/getAuthOptions.js';
31
+ import setSentryUser from './sentry/setSentryUser.js';
32
+
33
+ let loggerConfig = {};
34
+ try {
35
+ loggerConfig = require('../../build/logger.json');
36
+ } catch {
37
+ // logger.json may not exist if logger is not configured
38
+ }
31
39
 
32
40
  const secrets = getSecretsFromEnv();
33
41
 
@@ -35,8 +43,9 @@ function apiWrapper(handler) {
35
43
  return async function wrappedHandler(req, res) {
36
44
  const context = {
37
45
  // Important to give absolute path so Next can trace build files
38
- rid: crypto.randomUUID(),
46
+ rid: uuid(),
39
47
  buildDirectory: path.join(process.cwd(), 'build'),
48
+ configDirectory: process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd(),
40
49
  config,
41
50
  connections,
42
51
  fileCache,
@@ -53,6 +62,11 @@ function apiWrapper(handler) {
53
62
  context.authOptions = getAuthOptions(context);
54
63
  if (!req.url.startsWith('/api/auth')) {
55
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
+ });
56
70
  }
57
71
  createApiContext(context);
58
72
  logRequest({ context });
@@ -61,7 +75,7 @@ function apiWrapper(handler) {
61
75
  // TODO: Log response time?
62
76
  return response;
63
77
  } catch (error) {
64
- logError({ error, context });
78
+ await logError({ error, context });
65
79
  res.status(500).json({ name: error.name, message: error.message });
66
80
  }
67
81
  };
@@ -14,21 +14,71 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- function logError({ context, error }) {
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
+ }
18
25
  try {
19
- const { user = {} } = context;
20
-
21
- context.logger.error({
22
- err: error,
23
- user: {
24
- id: user.id,
25
- roles: user.roles,
26
- sub: user.sub,
27
- session_id: user.session_id,
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 }) {
43
+ try {
44
+ const message = error?.message || 'Unknown error';
45
+ const isServiceError = error?.isServiceError === true;
46
+
47
+ // For service errors, don't resolve config location (not a config issue)
48
+ const location = isServiceError ? null : await resolveErrorConfigLocation(context, error);
49
+
50
+ // Human-readable console output (single log entry)
51
+ const errorType = isServiceError ? 'Service Error' : 'Config Error';
52
+ const source = location?.source ? `${location.source} at ${location.config}` : '';
53
+ const link = location?.link || '';
54
+
55
+ if (isServiceError) {
56
+ console.error(`[${errorType}] ${message}`);
57
+ } else {
58
+ console.error(`[${errorType}] ${message}\n ${source}\n ${link}`);
59
+ }
60
+
61
+ // Structured logging
62
+ context.logger.error(
63
+ {
64
+ event: isServiceError ? 'service_error' : 'config_error',
65
+ errorName: error?.name || 'Error',
66
+ errorMessage: message,
67
+ isServiceError,
68
+ pageId: context.pageId || null,
69
+ timestamp: new Date().toISOString(),
70
+ source: location?.source || null,
71
+ config: location?.config || null,
72
+ link: location?.link || null,
28
73
  },
29
- url: context.req.url,
30
- method: context.req.method,
31
- resolvedUrl: context.nextContext?.resolvedUrl,
74
+ message
75
+ );
76
+
77
+ // Capture error to Sentry (no-op if Sentry not configured)
78
+ captureSentryError({
79
+ error,
80
+ context,
81
+ configLocation: location,
32
82
  });
33
83
  } catch (e) {
34
84
  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;
@@ -14,40 +14,12 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import path from 'path';
18
17
  import build from '@lowdefy/build';
19
18
  import createCustomPluginTypesMap from '../utils/createCustomPluginTypesMap.mjs';
20
19
 
21
20
  function lowdefyBuild({ directories, logger, options }) {
22
- // Persistent state for incremental builds
23
- const buildState = {
24
- dependencyGraph: new Map(),
25
- parsedContentCache: new Map(),
26
- refCache: new Map(),
27
- pathToRefHashes: new Map(), // Maps file path -> Set of ref hashes for that path
28
- };
29
-
30
- return async ({ changedFiles = [] } = {}) => {
31
- const isIncremental = changedFiles.length > 0 && buildState.dependencyGraph.size > 0;
32
- if (isIncremental) {
33
- logger.info(
34
- { print: 'spin' },
35
- `Rebuilding config (${changedFiles.length} file(s) changed)...`
36
- );
37
- } else {
38
- logger.info({ print: 'spin' }, 'Building config...');
39
- // Clear caches for full rebuild
40
- buildState.dependencyGraph.clear();
41
- buildState.parsedContentCache.clear();
42
- buildState.refCache.clear();
43
- buildState.pathToRefHashes.clear();
44
- }
45
-
46
- // Convert absolute paths to relative paths (relative to config directory)
47
- const relativeChangedFiles = changedFiles.map((filePath) =>
48
- path.relative(directories.config, filePath)
49
- );
50
-
21
+ return async () => {
22
+ logger.info({ print: 'spin' }, 'Building config...');
51
23
  const customTypesMap = await createCustomPluginTypesMap({ directories, logger });
52
24
  await build({
53
25
  customTypesMap,
@@ -55,9 +27,6 @@ function lowdefyBuild({ directories, logger, options }) {
55
27
  logger,
56
28
  refResolver: options.refResolver,
57
29
  stage: 'dev',
58
- changedFiles: isIncremental ? relativeChangedFiles : [],
59
- // Pass persistent state for incremental builds
60
- buildState,
61
30
  });
62
31
  logger.info({ print: 'log' }, 'Built config.');
63
32
  };
package/manager/run.mjs CHANGED
@@ -76,20 +76,10 @@ The run script does the following:
76
76
  pinging the /api/ping route, until it detects a new server has started, and then reloads the window.
77
77
  */
78
78
 
79
- /* TODO:
80
- Not killing server on errors properly
81
- when:
82
- - initial build fails
83
- */
84
-
85
79
  const context = await getContext();
86
80
 
87
81
  try {
88
- try {
89
- await context.initialBuild();
90
- } catch (error) {
91
- context.logger.error(error);
92
- }
82
+ await context.initialBuild();
93
83
 
94
84
  // We are not waiting for the startWatchers promise to resolve (all watchers have fired the ready event)
95
85
  // because chokidar sometimes doesn't fire this event, and it seems like there isn't an issue with not waiting.
@@ -23,10 +23,9 @@ function lowdefyBuildWatcher(context) {
23
23
  path.isAbsolute(item) ? item : path.resolve(context.directories.config, item);
24
24
 
25
25
  const callback = async (filePaths) => {
26
- const flatFilePaths = filePaths.flat();
27
- const lowdefyYamlModified = flatFilePaths.some(
28
- (filePath) => filePath.includes('lowdefy.yaml') || filePath.includes('lowdefy.yml')
29
- );
26
+ const lowdefyYamlModified = filePaths
27
+ .flat()
28
+ .some((filePath) => filePath.includes('lowdefy.yaml') || filePath.includes('lowdefy.yml'));
30
29
  if (lowdefyYamlModified) {
31
30
  const lowdefyVersion = await getLowdefyVersion(context);
32
31
  if (lowdefyVersion !== context.version && lowdefyVersion !== 'local') {
@@ -36,9 +35,7 @@ function lowdefyBuildWatcher(context) {
36
35
  }
37
36
  }
38
37
 
39
- // Pass changed files for incremental build (skip if lowdefy.yaml changed - needs full rebuild)
40
- const changedFiles = lowdefyYamlModified ? [] : flatFilePaths;
41
- await context.lowdefyBuild({ changedFiles });
38
+ await context.lowdefyBuild();
42
39
  context.reloadClients();
43
40
  };
44
41
  return setupWatcher({
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
- module.exports = withLess({
5
+ const nextConfig = withLess({
5
6
  basePath: lowdefyConfig.basePath,
6
7
  // reactStrictMode: true,
7
8
  webpack: (config, { isServer }) => {
@@ -19,7 +20,6 @@ module.exports = withLess({
19
20
  }
20
21
  return config;
21
22
  },
22
- swcMinify: false,
23
23
  compress: false,
24
24
  outputFileTracing: false,
25
25
  poweredByHeader: false,
@@ -29,3 +29,13 @@ module.exports = withLess({
29
29
  ignoreDuringBuilds: true,
30
30
  },
31
31
  });
32
+
33
+ // Only wrap with Sentry if SENTRY_DSN is configured
34
+ // This enables source map uploads when SENTRY_AUTH_TOKEN is present
35
+ module.exports = process.env.SENTRY_DSN
36
+ ? withSentryConfig(nextConfig, {
37
+ // Sentry options
38
+ silent: true,
39
+ hideSourceMaps: true,
40
+ })
41
+ : nextConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server-dev",
3
- "version": "0.0.0-experimental-20251203205559",
3
+ "version": "0.0.0-experimental-20260113081624",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -36,33 +36,34 @@
36
36
  ".npmrc"
37
37
  ],
38
38
  "dependencies": {
39
- "@lowdefy/actions-core": "0.0.0-experimental-20251203205559",
40
- "@lowdefy/api": "0.0.0-experimental-20251203205559",
41
- "@lowdefy/block-utils": "0.0.0-experimental-20251203205559",
42
- "@lowdefy/blocks-aggrid": "0.0.0-experimental-20251203205559",
43
- "@lowdefy/blocks-antd": "0.0.0-experimental-20251203205559",
44
- "@lowdefy/blocks-basic": "0.0.0-experimental-20251203205559",
45
- "@lowdefy/blocks-color-selectors": "0.0.0-experimental-20251203205559",
46
- "@lowdefy/blocks-echarts": "0.0.0-experimental-20251203205559",
47
- "@lowdefy/blocks-loaders": "0.0.0-experimental-20251203205559",
48
- "@lowdefy/blocks-markdown": "0.0.0-experimental-20251203205559",
49
- "@lowdefy/blocks-qr": "0.0.0-experimental-20251203205559",
50
- "@lowdefy/build": "0.0.0-experimental-20251203205559",
51
- "@lowdefy/client": "0.0.0-experimental-20251203205559",
52
- "@lowdefy/connection-axios-http": "0.0.0-experimental-20251203205559",
53
- "@lowdefy/engine": "0.0.0-experimental-20251203205559",
54
- "@lowdefy/helpers": "0.0.0-experimental-20251203205559",
55
- "@lowdefy/layout": "0.0.0-experimental-20251203205559",
56
- "@lowdefy/node-utils": "0.0.0-experimental-20251203205559",
57
- "@lowdefy/operators-change-case": "0.0.0-experimental-20251203205559",
58
- "@lowdefy/operators-diff": "0.0.0-experimental-20251203205559",
59
- "@lowdefy/operators-js": "0.0.0-experimental-20251203205559",
60
- "@lowdefy/operators-moment": "0.0.0-experimental-20251203205559",
61
- "@lowdefy/operators-mql": "0.0.0-experimental-20251203205559",
62
- "@lowdefy/operators-nunjucks": "0.0.0-experimental-20251203205559",
63
- "@lowdefy/operators-uuid": "0.0.0-experimental-20251203205559",
64
- "@lowdefy/operators-yaml": "0.0.0-experimental-20251203205559",
65
- "@lowdefy/plugin-next-auth": "0.0.0-experimental-20251203205559",
39
+ "@lowdefy/actions-core": "0.0.0-experimental-20260113081624",
40
+ "@lowdefy/api": "0.0.0-experimental-20260113081624",
41
+ "@lowdefy/block-utils": "0.0.0-experimental-20260113081624",
42
+ "@lowdefy/blocks-aggrid": "0.0.0-experimental-20260113081624",
43
+ "@lowdefy/blocks-antd": "0.0.0-experimental-20260113081624",
44
+ "@lowdefy/blocks-basic": "0.0.0-experimental-20260113081624",
45
+ "@lowdefy/blocks-color-selectors": "0.0.0-experimental-20260113081624",
46
+ "@lowdefy/blocks-echarts": "0.0.0-experimental-20260113081624",
47
+ "@lowdefy/blocks-loaders": "0.0.0-experimental-20260113081624",
48
+ "@lowdefy/blocks-markdown": "0.0.0-experimental-20260113081624",
49
+ "@lowdefy/blocks-qr": "0.0.0-experimental-20260113081624",
50
+ "@lowdefy/build": "0.0.0-experimental-20260113081624",
51
+ "@lowdefy/client": "0.0.0-experimental-20260113081624",
52
+ "@lowdefy/connection-axios-http": "0.0.0-experimental-20260113081624",
53
+ "@lowdefy/engine": "0.0.0-experimental-20260113081624",
54
+ "@lowdefy/helpers": "0.0.0-experimental-20260113081624",
55
+ "@lowdefy/layout": "0.0.0-experimental-20260113081624",
56
+ "@lowdefy/node-utils": "0.0.0-experimental-20260113081624",
57
+ "@lowdefy/operators-change-case": "0.0.0-experimental-20260113081624",
58
+ "@lowdefy/operators-diff": "0.0.0-experimental-20260113081624",
59
+ "@lowdefy/operators-js": "0.0.0-experimental-20260113081624",
60
+ "@lowdefy/operators-moment": "0.0.0-experimental-20260113081624",
61
+ "@lowdefy/operators-mql": "0.0.0-experimental-20260113081624",
62
+ "@lowdefy/operators-nunjucks": "0.0.0-experimental-20260113081624",
63
+ "@lowdefy/operators-uuid": "0.0.0-experimental-20260113081624",
64
+ "@lowdefy/operators-yaml": "0.0.0-experimental-20260113081624",
65
+ "@lowdefy/plugin-next-auth": "0.0.0-experimental-20260113081624",
66
+ "@sentry/nextjs": "8.53.0",
66
67
  "chokidar": "3.5.3",
67
68
  "dotenv": "16.3.1",
68
69
  "next": "13.5.4",
@@ -74,6 +75,7 @@
74
75
  "react-dom": "18.2.0",
75
76
  "react-icons": "4.12.0",
76
77
  "swr": "2.2.4",
78
+ "uuid": "13.0.0",
77
79
  "yaml": "2.3.4",
78
80
  "yargs": "17.7.2"
79
81
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server-dev",
3
- "version": "0.0.0-experimental-20251203205559",
3
+ "version": "0.0.0-experimental-20260113081624",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -43,33 +43,34 @@
43
43
  "prepublishOnly": "pnpm build"
44
44
  },
45
45
  "dependencies": {
46
- "@lowdefy/actions-core": "0.0.0-experimental-20251203205559",
47
- "@lowdefy/api": "0.0.0-experimental-20251203205559",
48
- "@lowdefy/block-utils": "0.0.0-experimental-20251203205559",
49
- "@lowdefy/blocks-aggrid": "0.0.0-experimental-20251203205559",
50
- "@lowdefy/blocks-antd": "0.0.0-experimental-20251203205559",
51
- "@lowdefy/blocks-basic": "0.0.0-experimental-20251203205559",
52
- "@lowdefy/blocks-color-selectors": "0.0.0-experimental-20251203205559",
53
- "@lowdefy/blocks-echarts": "0.0.0-experimental-20251203205559",
54
- "@lowdefy/blocks-loaders": "0.0.0-experimental-20251203205559",
55
- "@lowdefy/blocks-markdown": "0.0.0-experimental-20251203205559",
56
- "@lowdefy/blocks-qr": "0.0.0-experimental-20251203205559",
57
- "@lowdefy/build": "0.0.0-experimental-20251203205559",
58
- "@lowdefy/client": "0.0.0-experimental-20251203205559",
59
- "@lowdefy/connection-axios-http": "0.0.0-experimental-20251203205559",
60
- "@lowdefy/engine": "0.0.0-experimental-20251203205559",
61
- "@lowdefy/helpers": "0.0.0-experimental-20251203205559",
62
- "@lowdefy/layout": "0.0.0-experimental-20251203205559",
63
- "@lowdefy/node-utils": "0.0.0-experimental-20251203205559",
64
- "@lowdefy/operators-change-case": "0.0.0-experimental-20251203205559",
65
- "@lowdefy/operators-diff": "0.0.0-experimental-20251203205559",
66
- "@lowdefy/operators-js": "0.0.0-experimental-20251203205559",
67
- "@lowdefy/operators-moment": "0.0.0-experimental-20251203205559",
68
- "@lowdefy/operators-mql": "0.0.0-experimental-20251203205559",
69
- "@lowdefy/operators-nunjucks": "0.0.0-experimental-20251203205559",
70
- "@lowdefy/operators-uuid": "0.0.0-experimental-20251203205559",
71
- "@lowdefy/operators-yaml": "0.0.0-experimental-20251203205559",
72
- "@lowdefy/plugin-next-auth": "0.0.0-experimental-20251203205559",
46
+ "@lowdefy/actions-core": "0.0.0-experimental-20260113081624",
47
+ "@lowdefy/api": "0.0.0-experimental-20260113081624",
48
+ "@lowdefy/block-utils": "0.0.0-experimental-20260113081624",
49
+ "@lowdefy/blocks-aggrid": "0.0.0-experimental-20260113081624",
50
+ "@lowdefy/blocks-antd": "0.0.0-experimental-20260113081624",
51
+ "@lowdefy/blocks-basic": "0.0.0-experimental-20260113081624",
52
+ "@lowdefy/blocks-color-selectors": "0.0.0-experimental-20260113081624",
53
+ "@lowdefy/blocks-echarts": "0.0.0-experimental-20260113081624",
54
+ "@lowdefy/blocks-loaders": "0.0.0-experimental-20260113081624",
55
+ "@lowdefy/blocks-markdown": "0.0.0-experimental-20260113081624",
56
+ "@lowdefy/blocks-qr": "0.0.0-experimental-20260113081624",
57
+ "@lowdefy/build": "0.0.0-experimental-20260113081624",
58
+ "@lowdefy/client": "0.0.0-experimental-20260113081624",
59
+ "@lowdefy/connection-axios-http": "0.0.0-experimental-20260113081624",
60
+ "@lowdefy/engine": "0.0.0-experimental-20260113081624",
61
+ "@lowdefy/helpers": "0.0.0-experimental-20260113081624",
62
+ "@lowdefy/layout": "0.0.0-experimental-20260113081624",
63
+ "@lowdefy/node-utils": "0.0.0-experimental-20260113081624",
64
+ "@lowdefy/operators-change-case": "0.0.0-experimental-20260113081624",
65
+ "@lowdefy/operators-diff": "0.0.0-experimental-20260113081624",
66
+ "@lowdefy/operators-js": "0.0.0-experimental-20260113081624",
67
+ "@lowdefy/operators-moment": "0.0.0-experimental-20260113081624",
68
+ "@lowdefy/operators-mql": "0.0.0-experimental-20260113081624",
69
+ "@lowdefy/operators-nunjucks": "0.0.0-experimental-20260113081624",
70
+ "@lowdefy/operators-uuid": "0.0.0-experimental-20260113081624",
71
+ "@lowdefy/operators-yaml": "0.0.0-experimental-20260113081624",
72
+ "@lowdefy/plugin-next-auth": "0.0.0-experimental-20260113081624",
73
+ "@sentry/nextjs": "8.53.0",
73
74
  "chokidar": "3.5.3",
74
75
  "dotenv": "16.3.1",
75
76
  "next": "13.5.4",
@@ -81,6 +82,7 @@
81
82
  "react-dom": "18.2.0",
82
83
  "react-icons": "4.12.0",
83
84
  "swr": "2.2.4",
85
+ "uuid": "13.0.0",
84
86
  "yaml": "2.3.4",
85
87
  "yargs": "17.7.2"
86
88
  },
package/pages/_app.js CHANGED
@@ -20,16 +20,40 @@ import dynamic from 'next/dynamic';
20
20
  import { ErrorBoundary } from '@lowdefy/block-utils';
21
21
 
22
22
  import Auth from '../lib/client/auth/Auth.js';
23
+ import initSentryClient from '../lib/client/sentry/initSentryClient.js';
24
+ import setSentryUser from '../lib/client/sentry/setSentryUser.js';
25
+
26
+ let loggerConfig = {};
27
+ try {
28
+ loggerConfig = require('../build/logger.json');
29
+ } catch {
30
+ // logger.json may not exist if Sentry is not configured
31
+ }
23
32
 
24
33
  // Must be in _app due to next specifications.
25
34
  import '../build/plugins/styles.less';
26
35
 
36
+ // Initialize Sentry client once on module load
37
+ initSentryClient({
38
+ sentryDsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
39
+ sentryConfig: loggerConfig.sentry,
40
+ });
41
+
27
42
  function App({ Component }) {
28
43
  const lowdefyRef = useRef({});
29
44
  return (
30
45
  <ErrorBoundary fullPage>
31
46
  <Suspense fallback="">
32
- <Auth>{(auth) => <Component auth={auth} lowdefy={lowdefyRef.current} />}</Auth>
47
+ <Auth>
48
+ {(auth) => {
49
+ // Set Sentry user context when auth changes
50
+ setSentryUser({
51
+ user: auth.session,
52
+ sentryConfig: loggerConfig.sentry,
53
+ });
54
+ return <Component auth={auth} lowdefy={lowdefyRef.current} />;
55
+ }}
56
+ </Auth>
33
57
  </Suspense>
34
58
  </ErrorBoundary>
35
59
  );
@@ -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);
@@ -31,7 +31,7 @@ const handler = async (req, res) => {
31
31
  try {
32
32
  res.write(`event: reload\ndata: ${JSON.stringify({})}\n\n`);
33
33
  } catch (e) {
34
- console.log(e);
34
+ console.error(e);
35
35
  }
36
36
  };
37
37
  watcher.on('add', () => reload());