@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.
@@ -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 = crypto.randomUUID();
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;
@@ -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: crypto.randomUUID(),
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
- 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
+ }
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
- context.logger.error({
22
- // TODO:
23
- // app_name
24
- // app_version
25
- // lowdefy_version
26
- // build_hash
27
- // config_hash
28
- err: error,
29
- user: {
30
- id: user.id,
31
- roles: user.roles,
32
- sub: user.sub,
33
- session_id: user.session_id,
34
- },
35
- url: context.req.url,
36
- method: context.req.method,
37
- resolvedUrl: context.nextContext?.resolvedUrl,
38
- hostname: context.req.hostname,
39
- headers: {
40
- 'accept-language': headers['accept-language'],
41
- 'sec-ch-ua-mobile': headers['sec-ch-ua-mobile'],
42
- 'sec-ch-ua-platform': headers['sec-ch-ua-platform'],
43
- 'sec-ch-ua': headers['sec-ch-ua'],
44
- 'user-agent': headers['user-agent'],
45
- host: headers.host,
46
- referer: headers.referer,
47
- // Non localhost headers
48
- 'x-forward-for': headers['x-forward-for'],
49
- // Vercel headers
50
- 'x-vercel-id': headers['x-vercel-id'],
51
- 'x-real-ip': headers['x-real-ip'],
52
- 'x-vercel-ip-country': headers['x-vercel-ip-country'],
53
- 'x-vercel-ip-country-region': headers['x-vercel-ip-country-region'],
54
- 'x-vercel-ip-city': headers['x-vercel-ip-city'],
55
- 'x-vercel-ip-latitude': headers['x-vercel-ip-latitude'],
56
- 'x-vercel-ip-longitude': headers['x-vercel-ip-longitude'],
57
- 'x-vercel-ip-timezone': headers['x-vercel-ip-timezone'],
58
- // Cloudflare headers
59
- 'cf-connecting-ip': headers['cf-connecting-ip'],
60
- 'cf-ray': headers['cf-ray'],
61
- 'cf-ipcountry': headers['cf-ipcountry'],
62
- 'cf-visitor': headers['cf-visitor'],
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: crypto.randomUUID(),
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
- module.exports = withLess({
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-20251203205559",
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-20251203205559",
40
- "@lowdefy/api": "0.0.0-experimental-20251203205559",
41
- "@lowdefy/block-utils": "0.0.0-experimental-20251203205559",
42
- "@lowdefy/blocks-antd": "0.0.0-experimental-20251203205559",
43
- "@lowdefy/blocks-basic": "0.0.0-experimental-20251203205559",
44
- "@lowdefy/blocks-loaders": "0.0.0-experimental-20251203205559",
45
- "@lowdefy/client": "0.0.0-experimental-20251203205559",
46
- "@lowdefy/helpers": "0.0.0-experimental-20251203205559",
47
- "@lowdefy/layout": "0.0.0-experimental-20251203205559",
48
- "@lowdefy/node-utils": "0.0.0-experimental-20251203205559",
49
- "@lowdefy/operators-js": "0.0.0-experimental-20251203205559",
50
- "@lowdefy/plugin-next-auth": "0.0.0-experimental-20251203205559",
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-20251203205559",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server",
3
- "version": "0.0.0-experimental-20251203205559",
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-20251203205559",
50
- "@lowdefy/api": "0.0.0-experimental-20251203205559",
51
- "@lowdefy/block-utils": "0.0.0-experimental-20251203205559",
52
- "@lowdefy/blocks-antd": "0.0.0-experimental-20251203205559",
53
- "@lowdefy/blocks-basic": "0.0.0-experimental-20251203205559",
54
- "@lowdefy/blocks-loaders": "0.0.0-experimental-20251203205559",
55
- "@lowdefy/client": "0.0.0-experimental-20251203205559",
56
- "@lowdefy/helpers": "0.0.0-experimental-20251203205559",
57
- "@lowdefy/layout": "0.0.0-experimental-20251203205559",
58
- "@lowdefy/node-utils": "0.0.0-experimental-20251203205559",
59
- "@lowdefy/operators-js": "0.0.0-experimental-20251203205559",
60
- "@lowdefy/plugin-next-auth": "0.0.0-experimental-20251203205559",
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-20251203205559",
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);