@prenta/admin 1.17.5 → 1.17.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @actuate-media/cms-admin
2
2
 
3
+ ## 1.17.6
4
+
5
+ ### Patch Changes
6
+
7
+ - aa62695: Tag admin Sentry events with `NEXT_PUBLIC_SENTRY_RELEASE`. New projects pass `NEXT_PUBLIC_SENTRY_DSN` into the admin and install `@sentry/node` so `SENTRY_DSN` reports API errors.
8
+
3
9
  ## 1.17.5
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=error-boundary.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-boundary.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/components/error-boundary.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment happy-dom
3
+ import { render, screen } from '@testing-library/react';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ const captureAdminException = vi.fn();
6
+ vi.mock('../../lib/sentry.js', () => ({
7
+ captureAdminException: (...args) => captureAdminException(...args),
8
+ }));
9
+ const { ErrorBoundary } = await import('../../components/ErrorBoundary.js');
10
+ function Boom() {
11
+ throw new Error('boundary boom');
12
+ }
13
+ describe('ErrorBoundary', () => {
14
+ it('reports a render error to Sentry (#407)', () => {
15
+ vi.spyOn(console, 'error').mockImplementation(() => { });
16
+ captureAdminException.mockClear();
17
+ render(_jsx(ErrorBoundary, { children: _jsx(Boom, {}) }));
18
+ expect(screen.getByRole('heading', { name: 'Something went wrong' })).toBeTruthy();
19
+ expect(captureAdminException).toHaveBeenCalledWith(expect.objectContaining({ message: 'boundary boom' }), expect.objectContaining({ componentStack: expect.any(String) }));
20
+ });
21
+ });
22
+ //# sourceMappingURL=error-boundary.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-boundary.test.js","sourceRoot":"","sources":["../../../src/__tests__/components/error-boundary.test.tsx"],"names":[],"mappings":";AAAA,gCAAgC;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAEjD,MAAM,qBAAqB,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;AAErC,EAAE,CAAC,IAAI,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,CAAC;IACpC,qBAAqB,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,qBAAqB,CAAC,GAAG,IAAI,CAAC;CAC9E,CAAC,CAAC,CAAA;AAEH,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,mCAAmC,CAAC,CAAA;AAE3E,SAAS,IAAI;IACX,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAA;AAClC,CAAC;AAED,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QACvD,qBAAqB,CAAC,SAAS,EAAE,CAAA;QAEjC,MAAM,CACJ,KAAC,aAAa,cACZ,KAAC,IAAI,KAAG,GACM,CACjB,CAAA;QAED,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;QAClF,MAAM,CAAC,qBAAqB,CAAC,CAAC,oBAAoB,CAChD,MAAM,CAAC,gBAAgB,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,EACrD,MAAM,CAAC,gBAAgB,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAChE,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sentry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentry.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/lib/sentry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,63 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const sentryMocks = vi.hoisted(() => ({
3
+ init: vi.fn(),
4
+ captureException: vi.fn(),
5
+ }));
6
+ vi.mock('@sentry/react', () => ({
7
+ init: sentryMocks.init,
8
+ captureException: sentryMocks.captureException,
9
+ }));
10
+ describe('admin Sentry', () => {
11
+ const originalRelease = process.env.NEXT_PUBLIC_SENTRY_RELEASE;
12
+ beforeEach(() => {
13
+ vi.resetModules();
14
+ sentryMocks.init.mockClear();
15
+ sentryMocks.captureException.mockClear();
16
+ delete process.env.NEXT_PUBLIC_SENTRY_RELEASE;
17
+ });
18
+ afterEach(() => {
19
+ if (originalRelease === undefined)
20
+ delete process.env.NEXT_PUBLIC_SENTRY_RELEASE;
21
+ else
22
+ process.env.NEXT_PUBLIC_SENTRY_RELEASE = originalRelease;
23
+ });
24
+ it('does not initialize without a DSN', async () => {
25
+ const { initAdminSentry } = await import('../../lib/sentry.js');
26
+ initAdminSentry(undefined);
27
+ initAdminSentry(' ');
28
+ expect(sentryMocks.init).not.toHaveBeenCalled();
29
+ });
30
+ it('initializes once and tags the release', async () => {
31
+ process.env.NEXT_PUBLIC_SENTRY_RELEASE = ' prenta@1.2.3 ';
32
+ const { initAdminSentry } = await import('../../lib/sentry.js');
33
+ initAdminSentry(' https://example@sentry.io/1 ');
34
+ initAdminSentry('https://example@sentry.io/1');
35
+ expect(sentryMocks.init).toHaveBeenCalledTimes(1);
36
+ expect(sentryMocks.init).toHaveBeenCalledWith(expect.objectContaining({
37
+ dsn: 'https://example@sentry.io/1',
38
+ release: 'prenta@1.2.3',
39
+ tracesSampleRate: 0,
40
+ }));
41
+ });
42
+ it('omits the release when NEXT_PUBLIC_SENTRY_RELEASE is unset', async () => {
43
+ const { initAdminSentry } = await import('../../lib/sentry.js');
44
+ initAdminSentry('https://example@sentry.io/1');
45
+ expect(sentryMocks.init).toHaveBeenCalledWith(expect.objectContaining({
46
+ dsn: 'https://example@sentry.io/1',
47
+ release: undefined,
48
+ }));
49
+ });
50
+ it('captures only after init, including the React component stack', async () => {
51
+ const { initAdminSentry, captureAdminException } = await import('../../lib/sentry.js');
52
+ const early = new Error('early');
53
+ captureAdminException(early);
54
+ expect(sentryMocks.captureException).not.toHaveBeenCalled();
55
+ initAdminSentry('https://example@sentry.io/1');
56
+ const err = new Error('boom');
57
+ captureAdminException(err, { componentStack: '\n at Foo' });
58
+ expect(sentryMocks.captureException).toHaveBeenCalledWith(err, {
59
+ contexts: { react: { componentStack: '\n at Foo' } },
60
+ });
61
+ });
62
+ });
63
+ //# sourceMappingURL=sentry.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentry.test.js","sourceRoot":"","sources":["../../../src/__tests__/lib/sentry.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sentry.d.ts","sourceRoot":"","sources":["../../src/lib/sentry.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAoB7D;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC3C,IAAI,CAWN"}
1
+ {"version":3,"file":"sentry.d.ts","sourceRoot":"","sources":["../../src/lib/sentry.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAyB7D;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC3C,IAAI,CAWN"}
@@ -4,9 +4,13 @@ let initialized = false;
4
4
  export function initAdminSentry(dsn) {
5
5
  if (initialized || !dsn?.trim())
6
6
  return;
7
+ // Next inlines NEXT_PUBLIC_* at the consumer build, including this
8
+ // reference inside the published admin package.
9
+ const release = process.env.NEXT_PUBLIC_SENTRY_RELEASE?.trim() || undefined;
7
10
  Sentry.init({
8
11
  dsn: dsn.trim(),
9
12
  environment: process.env.NODE_ENV ?? 'development',
13
+ release,
10
14
  tracesSampleRate: 0,
11
15
  beforeSend(event) {
12
16
  const str = JSON.stringify(event);
@@ -1 +1 @@
1
- {"version":3,"file":"sentry.js","sourceRoot":"","sources":["../../src/lib/sentry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,IAAI,WAAW,GAAG,KAAK,CAAA;AAEvB,iFAAiF;AACjF,MAAM,UAAU,eAAe,CAAC,GAAuB;IACrD,IAAI,WAAW,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE;QAAE,OAAM;IAEvC,MAAM,CAAC,IAAI,CAAC;QACV,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE;QACf,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa;QAClD,gBAAgB,EAAE,CAAC;QACnB,UAAU,CAAC,KAAK;YACd,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;YACjC,IAAI,qCAAqC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACpD,OAAO;oBACL,GAAG,KAAK;oBACR,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;oBACzB,WAAW,EAAE,EAAE;iBAChB,CAAA;YACH,CAAC;YACD,OAAO,KAAK,CAAA;QACd,CAAC;KACF,CAAC,CAAA;IACF,WAAW,GAAG,IAAI,CAAA;AACpB,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,qBAAqB,CACnC,KAAY,EACZ,OAA4C;IAE5C,IAAI,CAAC;QACH,IAAI,CAAC,WAAW;YAAE,OAAM;QACxB,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE;YAC7B,QAAQ,EAAE,OAAO,EAAE,cAAc;gBAC/B,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE;gBACvD,CAAC,CAAC,SAAS;SACd,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,+CAA+C;IACjD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"sentry.js","sourceRoot":"","sources":["../../src/lib/sentry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,IAAI,WAAW,GAAG,KAAK,CAAA;AAEvB,iFAAiF;AACjF,MAAM,UAAU,eAAe,CAAC,GAAuB;IACrD,IAAI,WAAW,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE;QAAE,OAAM;IAEvC,mEAAmE;IACnE,gDAAgD;IAChD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,IAAI,EAAE,IAAI,SAAS,CAAA;IAE3E,MAAM,CAAC,IAAI,CAAC;QACV,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE;QACf,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa;QAClD,OAAO;QACP,gBAAgB,EAAE,CAAC;QACnB,UAAU,CAAC,KAAK;YACd,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;YACjC,IAAI,qCAAqC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACpD,OAAO;oBACL,GAAG,KAAK;oBACR,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;oBACzB,WAAW,EAAE,EAAE;iBAChB,CAAA;YACH,CAAC;YACD,OAAO,KAAK,CAAA;QACd,CAAC;KACF,CAAC,CAAA;IACF,WAAW,GAAG,IAAI,CAAA;AACpB,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,qBAAqB,CACnC,KAAY,EACZ,OAA4C;IAE5C,IAAI,CAAC;QACH,IAAI,CAAC,WAAW;YAAE,OAAM;QACxB,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE;YAC7B,QAAQ,EAAE,OAAO,EAAE,cAAc;gBAC/B,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE;gBACvD,CAAC,CAAC,SAAS;SACd,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,+CAA+C;IACjD,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prenta/admin",
3
- "version": "1.17.5",
3
+ "version": "1.17.6",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -99,14 +99,14 @@
99
99
  "typescript": "^5.7.0",
100
100
  "vitest": "^4.1.0",
101
101
  "@prenta/component-blocks": "0.3.1",
102
+ "@prenta/core": "7.2.1",
102
103
  "@prenta/plugin-ai": "3.0.0",
103
104
  "@prenta/plugin-blocks": "0.2.0",
104
- "@prenta/core": "7.2.1",
105
105
  "@prenta/plugin-commerce": "0.2.1",
106
106
  "@prenta/plugin-forms": "0.1.0",
107
- "@prenta/plugin-seo": "1.0.0",
108
- "@prenta/plugin-navigation": "0.3.0",
109
107
  "@prenta/plugin-media": "0.2.0",
108
+ "@prenta/plugin-navigation": "0.3.0",
109
+ "@prenta/plugin-seo": "1.0.0",
110
110
  "@prenta/sections-react": "0.17.46"
111
111
  },
112
112
  "scripts": {
@@ -0,0 +1,34 @@
1
+ // @vitest-environment happy-dom
2
+ import { render, screen } from '@testing-library/react'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+
5
+ const captureAdminException = vi.fn()
6
+
7
+ vi.mock('../../lib/sentry.js', () => ({
8
+ captureAdminException: (...args: unknown[]) => captureAdminException(...args),
9
+ }))
10
+
11
+ const { ErrorBoundary } = await import('../../components/ErrorBoundary.js')
12
+
13
+ function Boom(): never {
14
+ throw new Error('boundary boom')
15
+ }
16
+
17
+ describe('ErrorBoundary', () => {
18
+ it('reports a render error to Sentry (#407)', () => {
19
+ vi.spyOn(console, 'error').mockImplementation(() => {})
20
+ captureAdminException.mockClear()
21
+
22
+ render(
23
+ <ErrorBoundary>
24
+ <Boom />
25
+ </ErrorBoundary>,
26
+ )
27
+
28
+ expect(screen.getByRole('heading', { name: 'Something went wrong' })).toBeTruthy()
29
+ expect(captureAdminException).toHaveBeenCalledWith(
30
+ expect.objectContaining({ message: 'boundary boom' }),
31
+ expect.objectContaining({ componentStack: expect.any(String) }),
32
+ )
33
+ })
34
+ })
@@ -0,0 +1,77 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ const sentryMocks = vi.hoisted(() => ({
4
+ init: vi.fn(),
5
+ captureException: vi.fn(),
6
+ }))
7
+
8
+ vi.mock('@sentry/react', () => ({
9
+ init: sentryMocks.init,
10
+ captureException: sentryMocks.captureException,
11
+ }))
12
+
13
+ describe('admin Sentry', () => {
14
+ const originalRelease = process.env.NEXT_PUBLIC_SENTRY_RELEASE
15
+
16
+ beforeEach(() => {
17
+ vi.resetModules()
18
+ sentryMocks.init.mockClear()
19
+ sentryMocks.captureException.mockClear()
20
+ delete process.env.NEXT_PUBLIC_SENTRY_RELEASE
21
+ })
22
+
23
+ afterEach(() => {
24
+ if (originalRelease === undefined) delete process.env.NEXT_PUBLIC_SENTRY_RELEASE
25
+ else process.env.NEXT_PUBLIC_SENTRY_RELEASE = originalRelease
26
+ })
27
+
28
+ it('does not initialize without a DSN', async () => {
29
+ const { initAdminSentry } = await import('../../lib/sentry.js')
30
+ initAdminSentry(undefined)
31
+ initAdminSentry(' ')
32
+ expect(sentryMocks.init).not.toHaveBeenCalled()
33
+ })
34
+
35
+ it('initializes once and tags the release', async () => {
36
+ process.env.NEXT_PUBLIC_SENTRY_RELEASE = ' prenta@1.2.3 '
37
+ const { initAdminSentry } = await import('../../lib/sentry.js')
38
+
39
+ initAdminSentry(' https://example@sentry.io/1 ')
40
+ initAdminSentry('https://example@sentry.io/1')
41
+
42
+ expect(sentryMocks.init).toHaveBeenCalledTimes(1)
43
+ expect(sentryMocks.init).toHaveBeenCalledWith(
44
+ expect.objectContaining({
45
+ dsn: 'https://example@sentry.io/1',
46
+ release: 'prenta@1.2.3',
47
+ tracesSampleRate: 0,
48
+ }),
49
+ )
50
+ })
51
+
52
+ it('omits the release when NEXT_PUBLIC_SENTRY_RELEASE is unset', async () => {
53
+ const { initAdminSentry } = await import('../../lib/sentry.js')
54
+ initAdminSentry('https://example@sentry.io/1')
55
+ expect(sentryMocks.init).toHaveBeenCalledWith(
56
+ expect.objectContaining({
57
+ dsn: 'https://example@sentry.io/1',
58
+ release: undefined,
59
+ }),
60
+ )
61
+ })
62
+
63
+ it('captures only after init, including the React component stack', async () => {
64
+ const { initAdminSentry, captureAdminException } = await import('../../lib/sentry.js')
65
+ const early = new Error('early')
66
+ captureAdminException(early)
67
+ expect(sentryMocks.captureException).not.toHaveBeenCalled()
68
+
69
+ initAdminSentry('https://example@sentry.io/1')
70
+ const err = new Error('boom')
71
+ captureAdminException(err, { componentStack: '\n at Foo' })
72
+
73
+ expect(sentryMocks.captureException).toHaveBeenCalledWith(err, {
74
+ contexts: { react: { componentStack: '\n at Foo' } },
75
+ })
76
+ })
77
+ })
package/src/lib/sentry.ts CHANGED
@@ -6,9 +6,14 @@ let initialized = false
6
6
  export function initAdminSentry(dsn: string | undefined): void {
7
7
  if (initialized || !dsn?.trim()) return
8
8
 
9
+ // Next inlines NEXT_PUBLIC_* at the consumer build, including this
10
+ // reference inside the published admin package.
11
+ const release = process.env.NEXT_PUBLIC_SENTRY_RELEASE?.trim() || undefined
12
+
9
13
  Sentry.init({
10
14
  dsn: dsn.trim(),
11
15
  environment: process.env.NODE_ENV ?? 'development',
16
+ release,
12
17
  tracesSampleRate: 0,
13
18
  beforeSend(event) {
14
19
  const str = JSON.stringify(event)