@magnet-cms/plugin-sentry 1.0.0

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,907 @@
1
+ import { Module, BadGatewayException, Get, Query, Controller, Inject, Injectable } from '@nestjs/common';
2
+ import { Plugin, RestrictedRoute } from '@magnet-cms/core';
3
+ import { APP_INTERCEPTOR } from '@nestjs/core';
4
+ export { SentryCron } from '@sentry/nestjs';
5
+
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
11
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
12
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
13
+ }) : x)(function(x) {
14
+ if (typeof require !== "undefined") return require.apply(this, arguments);
15
+ throw Error('Dynamic require of "' + x + '" is not supported');
16
+ });
17
+ var __esm = (fn, res) => function __init() {
18
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
19
+ };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, { get: all[name], enumerable: true });
23
+ };
24
+ var __copyProps = (to, from, except, desc) => {
25
+ if (from && typeof from === "object" || typeof from === "function") {
26
+ for (let key of __getOwnPropNames(from))
27
+ if (!__hasOwnProp.call(to, key) && key !== except)
28
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
29
+ }
30
+ return to;
31
+ };
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+
34
+ // src/backend/constants.ts
35
+ var SENTRY_OPTIONS;
36
+ var init_constants = __esm({
37
+ "src/backend/constants.ts"() {
38
+ SENTRY_OPTIONS = "SENTRY_OPTIONS";
39
+ }
40
+ });
41
+ function _ts_decorate(decorators, target, key, desc) {
42
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
43
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
44
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
45
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
46
+ }
47
+ function _ts_metadata(k, v) {
48
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
49
+ }
50
+ function _ts_param(paramIndex, decorator) {
51
+ return function(target, key) {
52
+ decorator(target, key, paramIndex);
53
+ };
54
+ }
55
+ function inferSentryApiBaseUrlFromDsn(dsn) {
56
+ if (!dsn?.trim()) return void 0;
57
+ try {
58
+ const host = new URL(dsn).hostname;
59
+ if (host.includes(".ingest.us.sentry.io")) return "https://us.sentry.io";
60
+ if (host.includes(".ingest.de.sentry.io")) return "https://de.sentry.io";
61
+ return void 0;
62
+ } catch {
63
+ return void 0;
64
+ }
65
+ }
66
+ function sentryApiFailureMessage(status, statusText) {
67
+ const base = `Sentry API error: ${status} ${statusText}`;
68
+ if (status === 401 || status === 403) {
69
+ return `${base}. Check SENTRY_AUTH_TOKEN scopes (e.g. org:read, project:read), that SENTRY_ORG and SENTRY_PROJECT are URL slugs (not numeric IDs), and SENTRY_URL matches your region (e.g. https://us.sentry.io when the DSN uses ingest.us.sentry.io).`;
70
+ }
71
+ if (status === 404) {
72
+ return `${base}. Check SENTRY_ORG and SENTRY_PROJECT slugs and SENTRY_URL.`;
73
+ }
74
+ return base;
75
+ }
76
+ var SentryApiService;
77
+ var init_sentry_api_service = __esm({
78
+ "src/backend/services/sentry-api.service.ts"() {
79
+ init_constants();
80
+ __name(_ts_decorate, "_ts_decorate");
81
+ __name(_ts_metadata, "_ts_metadata");
82
+ __name(_ts_param, "_ts_param");
83
+ __name(inferSentryApiBaseUrlFromDsn, "inferSentryApiBaseUrlFromDsn");
84
+ __name(sentryApiFailureMessage, "sentryApiFailureMessage");
85
+ SentryApiService = class {
86
+ static {
87
+ __name(this, "SentryApiService");
88
+ }
89
+ cache = /* @__PURE__ */ new Map();
90
+ scopeCache;
91
+ baseUrl;
92
+ authToken;
93
+ organization;
94
+ project;
95
+ constructor(config) {
96
+ this.baseUrl = config.sentryUrl ?? inferSentryApiBaseUrlFromDsn(config.dsn) ?? "https://sentry.io";
97
+ this.authToken = config.authToken;
98
+ this.organization = config.organization;
99
+ this.project = config.project;
100
+ }
101
+ /** Returns true when all required fields for API access are configured. */
102
+ isConfigured() {
103
+ return !!(this.authToken && this.organization && this.project);
104
+ }
105
+ /** Returns true when org-level API access is configured (no project required). */
106
+ isOrgConfigured() {
107
+ return !!(this.authToken && this.organization);
108
+ }
109
+ get orgSlug() {
110
+ return this.organization;
111
+ }
112
+ get projectSlug() {
113
+ return this.project;
114
+ }
115
+ /**
116
+ * Fetch org-level aggregate stats across all projects.
117
+ * Uses the org issues endpoint — no project slug required.
118
+ */
119
+ async getOrgStats() {
120
+ if (!this.isOrgConfigured()) {
121
+ throw new Error("Sentry API not configured: authToken and organization are required");
122
+ }
123
+ const issuesUrl = `${this.baseUrl}/api/0/organizations/${this.organization}/issues/?query=is:unresolved&limit=100`;
124
+ const issues = await this.fetchCached(issuesUrl);
125
+ const unresolvedIssues = issues.length;
126
+ const totalErrors = issues.reduce((sum, issue) => sum + Number(issue.count || 0), 0);
127
+ const cutoff24h = Date.now() - 864e5;
128
+ const errorsLast24h = issues.filter((issue) => new Date(issue.lastSeen).getTime() > cutoff24h).length;
129
+ return {
130
+ totalErrors,
131
+ unresolvedIssues,
132
+ errorsLast24h
133
+ };
134
+ }
135
+ /**
136
+ * Fetch issues from the Sentry organization (all projects).
137
+ * @param query - Optional Sentry search query (default: 'is:unresolved')
138
+ */
139
+ async getOrgIssues(query = "is:unresolved") {
140
+ if (!this.isOrgConfigured()) {
141
+ throw new Error("Sentry API not configured: authToken and organization are required");
142
+ }
143
+ const url = `${this.baseUrl}/api/0/organizations/${this.organization}/issues/?query=${encodeURIComponent(query)}&limit=25`;
144
+ return this.fetchCached(url);
145
+ }
146
+ /**
147
+ * Fetch project-level stats: total errors, unresolved issues, errors last 24h.
148
+ * Derives all stats from the issues endpoint to keep it a single API call.
149
+ * @param projectSlug - Override the configured project slug.
150
+ * Throws if not configured.
151
+ */
152
+ async getProjectStats(projectSlug) {
153
+ const project = projectSlug ?? this.project;
154
+ if (!this.authToken || !this.organization || !project) {
155
+ throw new Error("Sentry API not configured: authToken, organization, and project are required");
156
+ }
157
+ const issuesUrl = `${this.baseUrl}/api/0/projects/${this.organization}/${project}/issues/?query=is:unresolved&limit=100`;
158
+ const issues = await this.fetchCached(issuesUrl);
159
+ const unresolvedIssues = issues.length;
160
+ const totalErrors = issues.reduce((sum, issue) => sum + Number(issue.count || 0), 0);
161
+ const cutoff24h = Date.now() - 864e5;
162
+ const errorsLast24h = issues.filter((issue) => new Date(issue.lastSeen).getTime() > cutoff24h).length;
163
+ return {
164
+ totalErrors,
165
+ unresolvedIssues,
166
+ errorsLast24h
167
+ };
168
+ }
169
+ /**
170
+ * Fetch issues from the Sentry project.
171
+ * @param query - Optional Sentry search query (default: 'is:unresolved')
172
+ * @param projectSlug - Override the configured project slug.
173
+ */
174
+ async getIssues(query = "is:unresolved", projectSlug) {
175
+ const project = projectSlug ?? this.project;
176
+ if (!this.authToken || !this.organization || !project) {
177
+ throw new Error("Sentry API not configured: authToken, organization, and project are required");
178
+ }
179
+ const url = `${this.baseUrl}/api/0/projects/${this.organization}/${project}/issues/?query=${encodeURIComponent(query)}&limit=25`;
180
+ return this.fetchCached(url);
181
+ }
182
+ /**
183
+ * Fetch all projects in the organization.
184
+ * Requires authToken and organization (project not required).
185
+ * Sets isActive=true for the project matching the configured SENTRY_PROJECT.
186
+ * Populates errorCount from embedded stats if the API returns them.
187
+ */
188
+ async getOrganizationProjects() {
189
+ if (!this.isOrgConfigured()) {
190
+ throw new Error("Sentry API not configured: authToken and organization are required");
191
+ }
192
+ const url = `${this.baseUrl}/api/0/organizations/${this.organization}/projects/?statsPeriod=24h`;
193
+ const raw = await this.fetchCached(url);
194
+ return raw.map((p) => ({
195
+ id: p.id,
196
+ slug: p.slug,
197
+ name: p.name,
198
+ platform: p.platform,
199
+ status: p.status,
200
+ dateCreated: p.dateCreated,
201
+ isActive: p.slug === this.project,
202
+ errorCount: p.stats ? p.stats.reduce((sum, [, count]) => sum + count, 0) : null
203
+ }));
204
+ }
205
+ /**
206
+ * Probe token scopes by making lightweight requests to known Sentry endpoints.
207
+ * Results are cached for 5 minutes.
208
+ *
209
+ * Probed scopes: org:read, project:read, event:read, alerts:read
210
+ * Non-200/non-403 responses are treated as scope unavailable.
211
+ * event:read probe is skipped when project is not configured.
212
+ */
213
+ async probeTokenScopes() {
214
+ const allFalse = {
215
+ orgRead: false,
216
+ projectRead: false,
217
+ eventRead: false,
218
+ alertsRead: false
219
+ };
220
+ if (!this.isOrgConfigured()) {
221
+ return allFalse;
222
+ }
223
+ if (this.scopeCache && this.scopeCache.expiresAt > Date.now()) {
224
+ return this.scopeCache.data;
225
+ }
226
+ const probe = /* @__PURE__ */ __name(async (url) => {
227
+ try {
228
+ const response = await fetch(url, {
229
+ headers: {
230
+ Authorization: `Bearer ${this.authToken}`
231
+ }
232
+ });
233
+ return response.status === 200;
234
+ } catch {
235
+ return false;
236
+ }
237
+ }, "probe");
238
+ const [orgRead, projectRead, alertsRead] = await Promise.all([
239
+ probe(`${this.baseUrl}/api/0/organizations/${this.organization}/`),
240
+ probe(`${this.baseUrl}/api/0/organizations/${this.organization}/projects/?per_page=1`),
241
+ probe(`${this.baseUrl}/api/0/organizations/${this.organization}/alert-rules/?per_page=1`)
242
+ ]);
243
+ const eventRead = this.isConfigured() ? await probe(`${this.baseUrl}/api/0/projects/${this.organization}/${this.project}/issues/?limit=1`) : false;
244
+ const scopes = {
245
+ orgRead,
246
+ projectRead,
247
+ eventRead,
248
+ alertsRead
249
+ };
250
+ this.scopeCache = {
251
+ data: scopes,
252
+ expiresAt: Date.now() + 3e5
253
+ };
254
+ return scopes;
255
+ }
256
+ async fetchCached(url) {
257
+ const cached = this.cache.get(url);
258
+ if (cached && cached.expiresAt > Date.now()) {
259
+ return cached.data;
260
+ }
261
+ const response = await fetch(url, {
262
+ headers: {
263
+ Authorization: `Bearer ${this.authToken}`,
264
+ "Content-Type": "application/json"
265
+ }
266
+ });
267
+ if (!response.ok) {
268
+ throw new BadGatewayException(sentryApiFailureMessage(response.status, response.statusText));
269
+ }
270
+ const data = await response.json();
271
+ this.cache.set(url, {
272
+ data,
273
+ expiresAt: Date.now() + 6e4
274
+ });
275
+ return data;
276
+ }
277
+ };
278
+ SentryApiService = _ts_decorate([
279
+ Injectable(),
280
+ _ts_param(0, Inject(SENTRY_OPTIONS)),
281
+ _ts_metadata("design:type", Function),
282
+ _ts_metadata("design:paramtypes", [
283
+ typeof Partial === "undefined" ? Object : Partial
284
+ ])
285
+ ], SentryApiService);
286
+ }
287
+ });
288
+ function _ts_decorate2(decorators, target, key, desc) {
289
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
290
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
291
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
292
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
293
+ }
294
+ function _ts_metadata2(k, v) {
295
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
296
+ }
297
+ function _ts_param2(paramIndex, decorator) {
298
+ return function(target, key) {
299
+ decorator(target, key, paramIndex);
300
+ };
301
+ }
302
+ function messageFromHttpException(e) {
303
+ const r = e.getResponse();
304
+ if (typeof r === "string") return r;
305
+ if (r && typeof r === "object" && "message" in r) {
306
+ const m = r.message;
307
+ return Array.isArray(m) ? m.join(" ") : String(m);
308
+ }
309
+ return e.message;
310
+ }
311
+ var SentryAdminController;
312
+ var init_sentry_admin_controller = __esm({
313
+ "src/backend/controllers/sentry-admin.controller.ts"() {
314
+ init_sentry_api_service();
315
+ __name(_ts_decorate2, "_ts_decorate");
316
+ __name(_ts_metadata2, "_ts_metadata");
317
+ __name(_ts_param2, "_ts_param");
318
+ __name(messageFromHttpException, "messageFromHttpException");
319
+ SentryAdminController = class {
320
+ static {
321
+ __name(this, "SentryAdminController");
322
+ }
323
+ sentryApi;
324
+ constructor(sentryApi) {
325
+ this.sentryApi = sentryApi;
326
+ }
327
+ /**
328
+ * GET /sentry/admin/stats
329
+ * Returns error metrics for the dashboard: totals, unresolved count, 24h errors.
330
+ * Optional ?project= overrides the configured SENTRY_PROJECT.
331
+ * Returns zeroes with isConfigured:false when auth token is not set.
332
+ */
333
+ async getStats(project) {
334
+ if (!this.sentryApi.isOrgConfigured()) {
335
+ return {
336
+ isConfigured: false,
337
+ totalErrors: 0,
338
+ unresolvedIssues: 0,
339
+ errorsLast24h: 0
340
+ };
341
+ }
342
+ try {
343
+ const stats = project ? await this.sentryApi.getProjectStats(project) : await this.sentryApi.getOrgStats();
344
+ return {
345
+ isConfigured: true,
346
+ ...stats
347
+ };
348
+ } catch (e) {
349
+ if (e instanceof BadGatewayException) {
350
+ return {
351
+ isConfigured: true,
352
+ apiError: messageFromHttpException(e),
353
+ totalErrors: 0,
354
+ unresolvedIssues: 0,
355
+ errorsLast24h: 0
356
+ };
357
+ }
358
+ throw e;
359
+ }
360
+ }
361
+ /**
362
+ * GET /sentry/admin/issues
363
+ * Returns list of Sentry issues. Optional ?query= for search, ?project= to override project.
364
+ */
365
+ async getIssues(query, project) {
366
+ if (!this.sentryApi.isOrgConfigured()) {
367
+ return [];
368
+ }
369
+ try {
370
+ return project ? await this.sentryApi.getIssues(query, project) : await this.sentryApi.getOrgIssues(query);
371
+ } catch (e) {
372
+ if (e instanceof BadGatewayException) {
373
+ return [];
374
+ }
375
+ throw e;
376
+ }
377
+ }
378
+ /**
379
+ * GET /sentry/admin/status
380
+ * Returns API connectivity status for the settings page.
381
+ */
382
+ async getStatus() {
383
+ const connected = this.sentryApi.isConfigured();
384
+ return {
385
+ connected,
386
+ organization: this.sentryApi.orgSlug,
387
+ project: this.sentryApi.projectSlug,
388
+ lastSync: connected ? (/* @__PURE__ */ new Date()).toISOString() : null
389
+ };
390
+ }
391
+ /**
392
+ * GET /sentry/admin/projects
393
+ * Returns all projects in the organization, with isActive flag for the
394
+ * configured project and errorCount from 24h stats.
395
+ * Returns empty array when org is not configured or Sentry API errors.
396
+ */
397
+ async getProjects() {
398
+ if (!this.sentryApi.isOrgConfigured()) {
399
+ return [];
400
+ }
401
+ try {
402
+ return await this.sentryApi.getOrganizationProjects();
403
+ } catch (e) {
404
+ if (e instanceof BadGatewayException) {
405
+ return [];
406
+ }
407
+ throw e;
408
+ }
409
+ }
410
+ /**
411
+ * GET /sentry/admin/scopes
412
+ * Returns detected token scope availability by probing known Sentry endpoints.
413
+ * Results are cached for 5 minutes on the service.
414
+ * Returns all-false when org is not configured.
415
+ */
416
+ async getScopes() {
417
+ const allFalse = {
418
+ orgRead: false,
419
+ projectRead: false,
420
+ eventRead: false,
421
+ alertsRead: false
422
+ };
423
+ if (!this.sentryApi.isOrgConfigured()) {
424
+ return allFalse;
425
+ }
426
+ try {
427
+ return await this.sentryApi.probeTokenScopes();
428
+ } catch (e) {
429
+ if (e instanceof BadGatewayException) {
430
+ return allFalse;
431
+ }
432
+ throw e;
433
+ }
434
+ }
435
+ };
436
+ _ts_decorate2([
437
+ Get("admin/stats"),
438
+ RestrictedRoute(),
439
+ _ts_param2(0, Query("project")),
440
+ _ts_metadata2("design:type", Function),
441
+ _ts_metadata2("design:paramtypes", [
442
+ String
443
+ ]),
444
+ _ts_metadata2("design:returntype", Promise)
445
+ ], SentryAdminController.prototype, "getStats", null);
446
+ _ts_decorate2([
447
+ Get("admin/issues"),
448
+ RestrictedRoute(),
449
+ _ts_param2(0, Query("query")),
450
+ _ts_param2(1, Query("project")),
451
+ _ts_metadata2("design:type", Function),
452
+ _ts_metadata2("design:paramtypes", [
453
+ Object,
454
+ String
455
+ ]),
456
+ _ts_metadata2("design:returntype", Promise)
457
+ ], SentryAdminController.prototype, "getIssues", null);
458
+ _ts_decorate2([
459
+ Get("admin/status"),
460
+ RestrictedRoute(),
461
+ _ts_metadata2("design:type", Function),
462
+ _ts_metadata2("design:paramtypes", []),
463
+ _ts_metadata2("design:returntype", Promise)
464
+ ], SentryAdminController.prototype, "getStatus", null);
465
+ _ts_decorate2([
466
+ Get("admin/projects"),
467
+ RestrictedRoute(),
468
+ _ts_metadata2("design:type", Function),
469
+ _ts_metadata2("design:paramtypes", []),
470
+ _ts_metadata2("design:returntype", Promise)
471
+ ], SentryAdminController.prototype, "getProjects", null);
472
+ _ts_decorate2([
473
+ Get("admin/scopes"),
474
+ RestrictedRoute(),
475
+ _ts_metadata2("design:type", Function),
476
+ _ts_metadata2("design:paramtypes", []),
477
+ _ts_metadata2("design:returntype", Promise)
478
+ ], SentryAdminController.prototype, "getScopes", null);
479
+ SentryAdminController = _ts_decorate2([
480
+ Controller("sentry"),
481
+ _ts_metadata2("design:type", Function),
482
+ _ts_metadata2("design:paramtypes", [
483
+ typeof SentryApiService === "undefined" ? Object : SentryApiService
484
+ ])
485
+ ], SentryAdminController);
486
+ }
487
+ });
488
+ function _ts_decorate3(decorators, target, key, desc) {
489
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
490
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
491
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
492
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
493
+ }
494
+ function _ts_metadata3(k, v) {
495
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
496
+ }
497
+ function _ts_param3(paramIndex, decorator) {
498
+ return function(target, key) {
499
+ decorator(target, key, paramIndex);
500
+ };
501
+ }
502
+ var SentryConfigController;
503
+ var init_sentry_config_controller = __esm({
504
+ "src/backend/controllers/sentry-config.controller.ts"() {
505
+ init_constants();
506
+ __name(_ts_decorate3, "_ts_decorate");
507
+ __name(_ts_metadata3, "_ts_metadata");
508
+ __name(_ts_param3, "_ts_param");
509
+ SentryConfigController = class {
510
+ static {
511
+ __name(this, "SentryConfigController");
512
+ }
513
+ options;
514
+ constructor(options) {
515
+ this.options = options;
516
+ }
517
+ /**
518
+ * Return the public Sentry config needed to initialize the Browser SDK
519
+ * in the admin UI feedback widget.
520
+ *
521
+ * Requires authentication — uses the standard Magnet admin guard.
522
+ */
523
+ getConfig() {
524
+ return {
525
+ dsn: this.options.dsn ?? "",
526
+ enabled: this.options.enabled ?? true,
527
+ environment: this.options.environment ?? process.env.NODE_ENV ?? "production"
528
+ };
529
+ }
530
+ };
531
+ _ts_decorate3([
532
+ Get("config"),
533
+ RestrictedRoute(),
534
+ _ts_metadata3("design:type", Function),
535
+ _ts_metadata3("design:paramtypes", []),
536
+ _ts_metadata3("design:returntype", typeof SentryClientConfig === "undefined" ? Object : SentryClientConfig)
537
+ ], SentryConfigController.prototype, "getConfig", null);
538
+ SentryConfigController = _ts_decorate3([
539
+ Controller("sentry"),
540
+ _ts_param3(0, Inject(SENTRY_OPTIONS)),
541
+ _ts_metadata3("design:type", Function),
542
+ _ts_metadata3("design:paramtypes", [
543
+ typeof SentryPluginConfig === "undefined" ? Object : SentryPluginConfig
544
+ ])
545
+ ], SentryConfigController);
546
+ }
547
+ });
548
+ function _ts_decorate4(decorators, target, key, desc) {
549
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
550
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
551
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
552
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
553
+ }
554
+ var SentryContextInterceptor;
555
+ var init_sentry_context_interceptor = __esm({
556
+ "src/backend/interceptors/sentry-context.interceptor.ts"() {
557
+ __name(_ts_decorate4, "_ts_decorate");
558
+ SentryContextInterceptor = class {
559
+ static {
560
+ __name(this, "SentryContextInterceptor");
561
+ }
562
+ intercept(context, next) {
563
+ if (context.getType() !== "http") {
564
+ return next.handle();
565
+ }
566
+ const request = context.switchToHttp().getRequest();
567
+ try {
568
+ const Sentry = __require("@sentry/nestjs");
569
+ if (!Sentry.getClient()) return next.handle();
570
+ if (request.user?.id) {
571
+ Sentry.setUser({
572
+ id: request.user.id,
573
+ ip_address: request.ip
574
+ });
575
+ } else {
576
+ Sentry.setUser({
577
+ ip_address: request.ip
578
+ });
579
+ }
580
+ try {
581
+ const { getEventContext } = __require("@magnet-cms/core");
582
+ const ctx = getEventContext();
583
+ if (ctx?.requestId) {
584
+ Sentry.setTag("requestId", ctx.requestId);
585
+ }
586
+ } catch {
587
+ }
588
+ } catch {
589
+ }
590
+ return next.handle();
591
+ }
592
+ };
593
+ SentryContextInterceptor = _ts_decorate4([
594
+ Injectable()
595
+ ], SentryContextInterceptor);
596
+ }
597
+ });
598
+
599
+ // src/backend/plugin.ts
600
+ var plugin_exports = {};
601
+ __export(plugin_exports, {
602
+ SentryPlugin: () => SentryPlugin
603
+ });
604
+ function _ts_decorate5(decorators, target, key, desc) {
605
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
606
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
607
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
608
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
609
+ }
610
+ var SentryPlugin;
611
+ var init_plugin = __esm({
612
+ "src/backend/plugin.ts"() {
613
+ __name(_ts_decorate5, "_ts_decorate");
614
+ SentryPlugin = class _SentryPlugin {
615
+ static {
616
+ __name(this, "SentryPlugin");
617
+ }
618
+ /** Resolved configuration — stored statically so lifecycle hooks can access it */
619
+ static _resolvedConfig;
620
+ /** Environment variables used by this plugin */
621
+ static envVars = [
622
+ {
623
+ name: "SENTRY_DSN",
624
+ required: true,
625
+ description: "Sentry Data Source Name (DSN) for error reporting"
626
+ }
627
+ ];
628
+ /**
629
+ * Create a configured plugin provider for MagnetModule.forRoot().
630
+ *
631
+ * Auto-resolves DSN from the SENTRY_DSN environment variable if not provided.
632
+ *
633
+ * @example
634
+ * ```typescript
635
+ * SentryPlugin.forRoot({
636
+ * tracesSampleRate: 0.1,
637
+ * environment: 'production',
638
+ * })
639
+ * ```
640
+ */
641
+ static forRoot(config) {
642
+ const resolvedConfig = {
643
+ dsn: config?.dsn ?? process.env.SENTRY_DSN,
644
+ tracesSampleRate: config?.tracesSampleRate ?? 0.1,
645
+ profileSessionSampleRate: config?.profileSessionSampleRate ?? 1,
646
+ environment: config?.environment ?? process.env.SENTRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development",
647
+ release: config?.release ?? process.env.SENTRY_RELEASE,
648
+ debug: config?.debug ?? false,
649
+ enabled: config?.enabled ?? true,
650
+ attachStacktrace: config?.attachStacktrace ?? true,
651
+ maxBreadcrumbs: config?.maxBreadcrumbs ?? 100,
652
+ authToken: config?.authToken ?? process.env.SENTRY_AUTH_TOKEN,
653
+ organization: config?.organization ?? process.env.SENTRY_ORG,
654
+ project: config?.project ?? process.env.SENTRY_PROJECT,
655
+ sentryUrl: config?.sentryUrl ?? process.env.SENTRY_URL
656
+ };
657
+ _SentryPlugin._resolvedConfig = resolvedConfig;
658
+ return {
659
+ type: "plugin",
660
+ plugin: _SentryPlugin,
661
+ options: {
662
+ ...resolvedConfig
663
+ },
664
+ envVars: _SentryPlugin.envVars
665
+ };
666
+ }
667
+ /**
668
+ * Called by the plugin lifecycle service after NestJS module initialization.
669
+ * Initializes the Sentry SDK if not already initialized (e.g., via instrument.ts).
670
+ *
671
+ * NOTE: For full performance auto-instrumentation (HTTP, DB query tracing),
672
+ * initialize Sentry before NestJS bootstrap by importing `instrument.ts`
673
+ * at the top of main.ts and calling `initSentryInstrumentation()`.
674
+ * Late initialization still captures errors and breadcrumbs correctly.
675
+ */
676
+ async onPluginInit() {
677
+ const config = _SentryPlugin._resolvedConfig;
678
+ if (!config?.enabled) return;
679
+ try {
680
+ const Sentry = __require("@sentry/nestjs");
681
+ if (Sentry.getClient()) return;
682
+ Sentry.init({
683
+ dsn: config.dsn,
684
+ tracesSampleRate: config.tracesSampleRate,
685
+ environment: config.environment,
686
+ release: config.release,
687
+ debug: config.debug,
688
+ enabled: config.enabled,
689
+ attachStacktrace: config.attachStacktrace,
690
+ maxBreadcrumbs: config.maxBreadcrumbs
691
+ });
692
+ } catch {
693
+ }
694
+ }
695
+ /**
696
+ * Called when the application shuts down.
697
+ * Flushes pending Sentry events before exit.
698
+ */
699
+ async onPluginDestroy() {
700
+ try {
701
+ const Sentry = __require("@sentry/nestjs");
702
+ await Sentry.close(2e3);
703
+ } catch {
704
+ }
705
+ }
706
+ };
707
+ SentryPlugin = _ts_decorate5([
708
+ Plugin({
709
+ name: "sentry",
710
+ description: "Sentry error tracking and performance monitoring for Magnet CMS",
711
+ version: "0.1.0",
712
+ module: /* @__PURE__ */ __name(() => (
713
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
714
+ (init_sentry_module(), __toCommonJS(sentry_module_exports)).SentryModule
715
+ ), "module"),
716
+ frontend: {
717
+ routes: [
718
+ {
719
+ path: "sentry",
720
+ componentId: "SentryDashboard",
721
+ requiresAuth: true,
722
+ children: [
723
+ {
724
+ path: "",
725
+ componentId: "SentryDashboard"
726
+ },
727
+ {
728
+ path: "issues",
729
+ componentId: "SentryIssues"
730
+ },
731
+ {
732
+ path: "settings",
733
+ componentId: "SentrySettings"
734
+ }
735
+ ]
736
+ }
737
+ ],
738
+ sidebar: [
739
+ {
740
+ id: "sentry",
741
+ title: "Sentry",
742
+ url: "/sentry",
743
+ icon: "AlertTriangle",
744
+ order: 80,
745
+ items: [
746
+ {
747
+ id: "sentry-dashboard",
748
+ title: "Dashboard",
749
+ url: "/sentry",
750
+ icon: "BarChart3"
751
+ },
752
+ {
753
+ id: "sentry-issues",
754
+ title: "Issues",
755
+ url: "/sentry/issues",
756
+ icon: "Bug"
757
+ },
758
+ {
759
+ id: "sentry-projects",
760
+ title: "Projects",
761
+ url: "/sentry/projects",
762
+ icon: "FolderKanban"
763
+ },
764
+ {
765
+ id: "sentry-settings",
766
+ title: "Settings",
767
+ url: "/sentry/settings",
768
+ icon: "Settings"
769
+ }
770
+ ]
771
+ }
772
+ ]
773
+ }
774
+ })
775
+ ], SentryPlugin);
776
+ }
777
+ });
778
+
779
+ // src/backend/sentry.module.ts
780
+ var sentry_module_exports = {};
781
+ __export(sentry_module_exports, {
782
+ SENTRY_OPTIONS: () => SENTRY_OPTIONS,
783
+ SentryModule: () => SentryModule
784
+ });
785
+ function _ts_decorate6(decorators, target, key, desc) {
786
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
787
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
788
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
789
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
790
+ }
791
+ var SentryModule;
792
+ var init_sentry_module = __esm({
793
+ "src/backend/sentry.module.ts"() {
794
+ init_constants();
795
+ init_sentry_admin_controller();
796
+ init_sentry_config_controller();
797
+ init_sentry_context_interceptor();
798
+ init_sentry_api_service();
799
+ init_constants();
800
+ __name(_ts_decorate6, "_ts_decorate");
801
+ SentryModule = class {
802
+ static {
803
+ __name(this, "SentryModule");
804
+ }
805
+ };
806
+ SentryModule = _ts_decorate6([
807
+ Module({
808
+ controllers: [
809
+ SentryConfigController,
810
+ SentryAdminController
811
+ ],
812
+ providers: [
813
+ {
814
+ provide: SENTRY_OPTIONS,
815
+ // useFactory (not useValue) — factory is called lazily during DI resolution,
816
+ // after SentryPlugin.forRoot() has stored the config on the static field.
817
+ useFactory: /* @__PURE__ */ __name(() => {
818
+ const { SentryPlugin: SentryPlugin2 } = (init_plugin(), __toCommonJS(plugin_exports));
819
+ return SentryPlugin2._resolvedConfig ?? {};
820
+ }, "useFactory")
821
+ },
822
+ {
823
+ provide: APP_INTERCEPTOR,
824
+ useClass: SentryContextInterceptor
825
+ },
826
+ SentryApiService
827
+ ],
828
+ exports: [
829
+ SentryApiService
830
+ ]
831
+ })
832
+ ], SentryModule);
833
+ }
834
+ });
835
+
836
+ // src/backend/instrumentation.ts
837
+ function initSentryInstrumentation(config) {
838
+ try {
839
+ const Sentry = __require("@sentry/nestjs");
840
+ if (Sentry.getClient()) return;
841
+ Sentry.init({
842
+ dsn: config?.dsn ?? process.env.SENTRY_DSN,
843
+ tracesSampleRate: config?.tracesSampleRate ?? 0.1,
844
+ profileSessionSampleRate: config?.profileSessionSampleRate ?? 1,
845
+ environment: config?.environment ?? process.env.SENTRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development",
846
+ release: config?.release ?? process.env.SENTRY_RELEASE,
847
+ debug: config?.debug ?? false,
848
+ enabled: config?.enabled ?? true,
849
+ attachStacktrace: config?.attachStacktrace ?? true,
850
+ maxBreadcrumbs: config?.maxBreadcrumbs ?? 100
851
+ });
852
+ } catch {
853
+ }
854
+ }
855
+ __name(initSentryInstrumentation, "initSentryInstrumentation");
856
+
857
+ // src/backend/helpers/span.ts
858
+ async function withSentrySpan(name, op, fn) {
859
+ try {
860
+ const Sentry = __require("@sentry/nestjs");
861
+ if (!Sentry.getClient()) return fn();
862
+ return await Sentry.startSpan({
863
+ name,
864
+ op
865
+ }, () => fn());
866
+ } catch {
867
+ return fn();
868
+ }
869
+ }
870
+ __name(withSentrySpan, "withSentrySpan");
871
+
872
+ // src/backend/decorators/sentry-span.decorator.ts
873
+ function SentrySpan(name, op = "function") {
874
+ return /* @__PURE__ */ __name(function sentrySpanDecorator(targetOrMethod, propertyKeyOrContext, descriptor) {
875
+ if (descriptor !== void 0 && typeof descriptor.value === "function") {
876
+ const originalMethod = descriptor.value;
877
+ const spanName = name ?? String(propertyKeyOrContext ?? "unknown");
878
+ descriptor.value = async function(...args) {
879
+ return withSentrySpan(spanName, op, () => originalMethod.apply(this, args));
880
+ };
881
+ return descriptor;
882
+ }
883
+ if (typeof targetOrMethod === "function") {
884
+ const originalMethod = targetOrMethod;
885
+ const ctx = propertyKeyOrContext;
886
+ const spanName = name ?? String(ctx?.name ?? "unknown");
887
+ return async function(...args) {
888
+ return withSentrySpan(spanName, op, () => originalMethod.apply(this, args));
889
+ };
890
+ }
891
+ }, "sentrySpanDecorator");
892
+ }
893
+ __name(SentrySpan, "SentrySpan");
894
+ function MagnetSentryCron(slug, options) {
895
+ return (target, propertyKey, descriptor) => {
896
+ const monitorSlug = slug ?? `${target.constructor.name}-${String(propertyKey)}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
897
+ try {
898
+ const { SentryCron: SentryCron2 } = __require("@sentry/nestjs");
899
+ return SentryCron2(monitorSlug, options)(target, propertyKey, descriptor) ?? descriptor;
900
+ } catch {
901
+ return descriptor;
902
+ }
903
+ };
904
+ }
905
+ __name(MagnetSentryCron, "MagnetSentryCron");
906
+
907
+ export { MagnetSentryCron, SENTRY_OPTIONS, SentryModule, SentryPlugin, SentrySpan, initSentryInstrumentation, init_constants, init_plugin, init_sentry_module, withSentrySpan };