alchemy 0.16.5 → 0.16.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.
@@ -0,0 +1,295 @@
1
+ import type { Context } from "../context.js";
2
+ import { Resource } from "../resource.js";
3
+ import type { Secret } from "../secret.js";
4
+ import { SentryApi } from "./api.js";
5
+
6
+ /**
7
+ * Properties for creating or updating a ClientKey
8
+ */
9
+ export interface ClientKeyProps {
10
+ /**
11
+ * The name of the key
12
+ */
13
+ name?: string;
14
+
15
+ /**
16
+ * Rate limit configuration
17
+ */
18
+ rateLimit?: {
19
+ /**
20
+ * Time window in seconds
21
+ */
22
+ window: number;
23
+
24
+ /**
25
+ * Maximum number of events allowed in the window
26
+ */
27
+ count: number;
28
+ };
29
+
30
+ /**
31
+ * The use case for the key
32
+ */
33
+ useCase?: "user" | "profiling" | "escalating_issues" | "tempest" | "demo";
34
+
35
+ /**
36
+ * The project slug that owns the key
37
+ */
38
+ project: string;
39
+
40
+ /**
41
+ * The organization ID or slug that owns the key
42
+ */
43
+ organization: string;
44
+
45
+ /**
46
+ * Auth token to use (overrides environment variable)
47
+ */
48
+ authToken?: Secret;
49
+
50
+ /**
51
+ * Whether to adopt an existing key with the same name if it exists
52
+ * If true and a key with the same name exists, it will be adopted rather than creating a new one
53
+ *
54
+ * @default false
55
+ */
56
+ adopt?: boolean;
57
+ }
58
+
59
+ /**
60
+ * Output returned after ClientKey creation/update
61
+ */
62
+ export interface ClientKey
63
+ extends Resource<"sentry::ClientKey">,
64
+ ClientKeyProps {
65
+ /**
66
+ * The ID of the key
67
+ */
68
+ id: string;
69
+
70
+ /**
71
+ * The label of the key
72
+ */
73
+ label: string;
74
+
75
+ /**
76
+ * The public key
77
+ */
78
+ public: string;
79
+
80
+ /**
81
+ * The secret key
82
+ */
83
+ secret: string;
84
+
85
+ /**
86
+ * The project ID
87
+ */
88
+ projectId: number;
89
+
90
+ /**
91
+ * Whether the key is active
92
+ */
93
+ isActive: boolean;
94
+
95
+ /**
96
+ * DSN configuration
97
+ */
98
+ dsn: {
99
+ secret: string;
100
+ public: string;
101
+ csp: string;
102
+ security: string;
103
+ minidump: string;
104
+ nel: string;
105
+ unreal: string;
106
+ cdn: string;
107
+ crons: string;
108
+ };
109
+
110
+ /**
111
+ * Browser SDK version
112
+ */
113
+ browserSdkVersion: string;
114
+
115
+ /**
116
+ * Browser SDK choices
117
+ */
118
+ browserSdk: {
119
+ choices: Array<[string, string]>;
120
+ };
121
+
122
+ /**
123
+ * Time at which the key was created
124
+ */
125
+ dateCreated: string;
126
+
127
+ /**
128
+ * Dynamic SDK loader options
129
+ */
130
+ dynamicSdkLoaderOptions: {
131
+ hasReplay: boolean;
132
+ hasPerformance: boolean;
133
+ hasDebug: boolean;
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Create and manage Sentry client keys
139
+ *
140
+ * @example
141
+ * // Create a basic Sentry client key:
142
+ * const key = await ClientKey("my-key", {
143
+ * name: "My Key",
144
+ * project: "my-project",
145
+ * organization: "my-org"
146
+ * });
147
+ *
148
+ * @example
149
+ * // Create a client key with rate limiting:
150
+ * const key = await ClientKey("rate-limited-key", {
151
+ * name: "Rate Limited Key",
152
+ * project: "my-project",
153
+ * organization: "my-org",
154
+ * rateLimit: {
155
+ * window: 3600, // 1 hour
156
+ * count: 1000 // 1000 events per hour
157
+ * }
158
+ * });
159
+ *
160
+ * @example
161
+ * // Create a client key for a specific use case:
162
+ * const key = await ClientKey("profiling-key", {
163
+ * name: "Profiling Key",
164
+ * project: "my-project",
165
+ * organization: "my-org",
166
+ * useCase: "profiling"
167
+ * });
168
+ *
169
+ * @example
170
+ * // Create or adopt an existing key with the same name:
171
+ * const key = await ClientKey("existing-key", {
172
+ * name: "Existing Key",
173
+ * project: "my-project",
174
+ * organization: "my-org",
175
+ * adopt: true
176
+ * });
177
+ */
178
+ export const ClientKey = Resource(
179
+ "sentry::ClientKey",
180
+ async function (
181
+ this: Context<ClientKey>,
182
+ id: string,
183
+ props: ClientKeyProps,
184
+ ): Promise<ClientKey> {
185
+ const api = new SentryApi({ authToken: props.authToken });
186
+
187
+ if (this.phase === "delete") {
188
+ try {
189
+ if (this.output?.id) {
190
+ const response = await api.delete(
191
+ `/projects/${props.organization}/${props.project}/keys/${this.output.id}/`,
192
+ );
193
+ if (!response.ok && response.status !== 404) {
194
+ console.error("Error deleting client key:", response.statusText);
195
+ }
196
+ }
197
+ } catch (error) {
198
+ console.error("Error deleting client key:", error);
199
+ }
200
+ return this.destroy();
201
+ } else {
202
+ try {
203
+ let response;
204
+
205
+ if (this.phase === "update" && this.output?.id) {
206
+ response = await api.put(
207
+ `/projects/${props.organization}/${props.project}/keys/${this.output.id}/`,
208
+ props,
209
+ );
210
+ } else {
211
+ try {
212
+ response = await api.post(
213
+ `/projects/${props.organization}/${props.project}/keys/`,
214
+ props,
215
+ );
216
+ } catch (error) {
217
+ // Check if this is a "key already exists" error and adopt is enabled
218
+ if (
219
+ props.adopt &&
220
+ error instanceof Error &&
221
+ error.message.includes("already exists") &&
222
+ props.name
223
+ ) {
224
+ console.log(
225
+ `Client key '${props.name}' already exists, adopting it`,
226
+ );
227
+ // Find the existing key by name
228
+ const existingKey = await findClientKeyByName(
229
+ api,
230
+ props.organization,
231
+ props.project,
232
+ props.name,
233
+ );
234
+ if (!existingKey) {
235
+ throw new Error(
236
+ `Failed to find existing client key '${props.name}' for adoption`,
237
+ );
238
+ }
239
+ response = await api.get(
240
+ `/projects/${props.organization}/${props.project}/keys/${existingKey.id}/`,
241
+ );
242
+ } else {
243
+ throw error;
244
+ }
245
+ }
246
+ }
247
+
248
+ if (!response.ok) {
249
+ throw new Error(`API error: ${response.statusText}`);
250
+ }
251
+
252
+ const data = (await response.json()) as Omit<
253
+ ClientKey,
254
+ keyof ClientKeyProps
255
+ >;
256
+ return this({
257
+ ...props,
258
+ id: data.id,
259
+ label: data.label,
260
+ public: data.public,
261
+ secret: data.secret,
262
+ projectId: data.projectId,
263
+ isActive: data.isActive,
264
+ dsn: data.dsn,
265
+ browserSdkVersion: data.browserSdkVersion,
266
+ browserSdk: data.browserSdk,
267
+ dateCreated: data.dateCreated,
268
+ dynamicSdkLoaderOptions: data.dynamicSdkLoaderOptions,
269
+ });
270
+ } catch (error) {
271
+ console.error("Error creating/updating client key:", error);
272
+ throw error;
273
+ }
274
+ }
275
+ },
276
+ );
277
+
278
+ /**
279
+ * Find a client key by name
280
+ */
281
+ async function findClientKeyByName(
282
+ api: SentryApi,
283
+ organization: string,
284
+ project: string,
285
+ name: string,
286
+ ): Promise<{ id: string } | null> {
287
+ const response = await api.get(`/projects/${organization}/${project}/keys/`);
288
+ if (!response.ok) {
289
+ throw new Error(`API error: ${response.statusText}`);
290
+ }
291
+
292
+ const keys = (await response.json()) as Array<{ id: string; name: string }>;
293
+ const key = keys.find((k) => k.name === name);
294
+ return key ? { id: key.id } : null;
295
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./api.js";
2
+ export * from "./client-key.js";
3
+ export * from "./project.js";
4
+ export * from "./team.js";
@@ -0,0 +1,420 @@
1
+ import type { Context } from "../context.js";
2
+ import { Resource } from "../resource.js";
3
+ import type { Secret } from "../secret.js";
4
+ import { SentryApi } from "./api.js";
5
+
6
+ /**
7
+ * Properties for creating or updating a Project
8
+ */
9
+ export interface ProjectProps {
10
+ /**
11
+ * The name for the project
12
+ */
13
+ name: string;
14
+
15
+ /**
16
+ * Uniquely identifies a project and is used for the interface
17
+ */
18
+ slug?: string;
19
+
20
+ /**
21
+ * The platform for the project
22
+ */
23
+ platform?: string;
24
+
25
+ /**
26
+ * Whether to alert on every new issue
27
+ */
28
+ defaultRules?: boolean;
29
+
30
+ /**
31
+ * The team slug that owns the project
32
+ */
33
+ team: string;
34
+
35
+ /**
36
+ * The organization ID or slug that owns the project
37
+ */
38
+ organization: string;
39
+
40
+ /**
41
+ * Auth token to use (overrides environment variable)
42
+ */
43
+ authToken?: Secret;
44
+
45
+ /**
46
+ * Whether to adopt an existing project with the same slug if it exists
47
+ * If true and a project with the same slug exists, it will be adopted rather than creating a new one
48
+ *
49
+ * @default false
50
+ */
51
+ adopt?: boolean;
52
+ }
53
+
54
+ /**
55
+ * Output returned after Project creation/update
56
+ */
57
+ export interface Project
58
+ extends Omit<Resource<"sentry::Project">, "team">,
59
+ Omit<ProjectProps, "team"> {
60
+ /**
61
+ * The ID of the project
62
+ */
63
+ id: string;
64
+
65
+ /**
66
+ * The team that owns the project
67
+ */
68
+ team: {
69
+ id: string;
70
+ name: string;
71
+ slug: string;
72
+ };
73
+
74
+ /**
75
+ * All teams that have access to the project
76
+ */
77
+ teams: Array<{
78
+ id: string;
79
+ name: string;
80
+ slug: string;
81
+ }>;
82
+
83
+ /**
84
+ * Whether the project is bookmarked
85
+ */
86
+ isBookmarked: boolean;
87
+
88
+ /**
89
+ * Whether the current user is a member of the project
90
+ */
91
+ isMember: boolean;
92
+
93
+ /**
94
+ * Access permissions for the project
95
+ */
96
+ access: string[];
97
+
98
+ /**
99
+ * Whether the current user has access to the project
100
+ */
101
+ hasAccess: boolean;
102
+
103
+ /**
104
+ * Time at which the project was created
105
+ */
106
+ dateCreated: string;
107
+
108
+ /**
109
+ * List of environments in the project
110
+ */
111
+ environments: string[];
112
+
113
+ /**
114
+ * Event processing status
115
+ */
116
+ eventProcessing: {
117
+ symbolicationDegraded: boolean;
118
+ };
119
+
120
+ /**
121
+ * List of features enabled for the project
122
+ */
123
+ features: string[];
124
+
125
+ /**
126
+ * Whether the project has received its first event
127
+ */
128
+ firstEvent: string | null;
129
+
130
+ /**
131
+ * Whether the project has received its first transaction event
132
+ */
133
+ firstTransactionEvent: boolean;
134
+
135
+ /**
136
+ * Whether the project has sessions
137
+ */
138
+ hasSessions: boolean;
139
+
140
+ /**
141
+ * Whether the project has profiles
142
+ */
143
+ hasProfiles: boolean;
144
+
145
+ /**
146
+ * Whether the project has replays
147
+ */
148
+ hasReplays: boolean;
149
+
150
+ /**
151
+ * Whether the project has flags
152
+ */
153
+ hasFlags: boolean;
154
+
155
+ /**
156
+ * Whether the project has monitors
157
+ */
158
+ hasMonitors: boolean;
159
+
160
+ /**
161
+ * Whether the project has feedback
162
+ */
163
+ hasFeedbacks: boolean;
164
+
165
+ /**
166
+ * Whether the project has new feedback
167
+ */
168
+ hasNewFeedbacks: boolean;
169
+
170
+ /**
171
+ * Whether the project has minified stack traces
172
+ */
173
+ hasMinifiedStackTrace: boolean;
174
+
175
+ /**
176
+ * Whether the project has HTTP insights
177
+ */
178
+ hasInsightsHttp: boolean;
179
+
180
+ /**
181
+ * Whether the project has database insights
182
+ */
183
+ hasInsightsDb: boolean;
184
+
185
+ /**
186
+ * Whether the project has asset insights
187
+ */
188
+ hasInsightsAssets: boolean;
189
+
190
+ /**
191
+ * Whether the project has app start insights
192
+ */
193
+ hasInsightsAppStart: boolean;
194
+
195
+ /**
196
+ * Whether the project has screen load insights
197
+ */
198
+ hasInsightsScreenLoad: boolean;
199
+
200
+ /**
201
+ * Whether the project has vitals insights
202
+ */
203
+ hasInsightsVitals: boolean;
204
+
205
+ /**
206
+ * Whether the project has cache insights
207
+ */
208
+ hasInsightsCaches: boolean;
209
+
210
+ /**
211
+ * Whether the project has queue insights
212
+ */
213
+ hasInsightsQueues: boolean;
214
+
215
+ /**
216
+ * Whether the project has LLM monitoring
217
+ */
218
+ hasInsightsLlmMonitoring: boolean;
219
+
220
+ /**
221
+ * List of platforms in the project
222
+ */
223
+ platforms: string[];
224
+
225
+ /**
226
+ * Latest release information
227
+ */
228
+ latestRelease: string | null;
229
+
230
+ /**
231
+ * Whether the project has user reports
232
+ */
233
+ hasUserReports: boolean;
234
+
235
+ /**
236
+ * Latest deployment information
237
+ */
238
+ latestDeploys: string | null;
239
+ }
240
+
241
+ /**
242
+ * Create and manage Sentry projects
243
+ *
244
+ * @example
245
+ * // Create a basic Sentry project:
246
+ * const project = await Project("my-project", {
247
+ * name: "My Project",
248
+ * team: "my-team",
249
+ * organization: "my-org"
250
+ * });
251
+ *
252
+ * @example
253
+ * // Create a project for a specific platform:
254
+ * const project = await Project("js-project", {
255
+ * name: "JavaScript Project",
256
+ * team: "my-team",
257
+ * organization: "my-org",
258
+ * platform: "javascript"
259
+ * });
260
+ *
261
+ * @example
262
+ * // Create a project with a custom slug and disabled default rules:
263
+ * const project = await Project("custom-project", {
264
+ * name: "Custom Project",
265
+ * team: "my-team",
266
+ * organization: "my-org",
267
+ * slug: "custom-project-slug",
268
+ * defaultRules: false
269
+ * });
270
+ *
271
+ * @example
272
+ * // Create or adopt an existing project with the same slug:
273
+ * const project = await Project("existing-project", {
274
+ * name: "Existing Project",
275
+ * team: "my-team",
276
+ * organization: "my-org",
277
+ * adopt: true
278
+ * });
279
+ */
280
+ export const Project = Resource(
281
+ "sentry::Project",
282
+ async function (
283
+ this: Context<Project>,
284
+ id: string,
285
+ props: ProjectProps,
286
+ ): Promise<Project> {
287
+ const api = new SentryApi({ authToken: props.authToken });
288
+
289
+ if (this.phase === "delete") {
290
+ try {
291
+ if (this.output?.id) {
292
+ const response = await api.delete(
293
+ `/projects/${props.organization}/${this.output.slug || this.output.id}/`,
294
+ );
295
+ if (!response.ok && response.status !== 404) {
296
+ console.error("Error deleting project:", response.statusText);
297
+ }
298
+ }
299
+ } catch (error) {
300
+ console.error("Error deleting project:", error);
301
+ }
302
+ return this.destroy();
303
+ } else {
304
+ try {
305
+ let response;
306
+
307
+ if (this.phase === "update" && this.output?.id) {
308
+ response = await api.put(
309
+ `/projects/${props.organization}/${this.output.slug || this.output.id}/`,
310
+ props,
311
+ );
312
+ } else {
313
+ try {
314
+ response = await api.post(
315
+ `/teams/${props.organization}/${props.team}/projects/`,
316
+ props,
317
+ );
318
+ } catch (error) {
319
+ // Check if this is a "project already exists" error and adopt is enabled
320
+ if (
321
+ props.adopt &&
322
+ error instanceof Error &&
323
+ error.message.includes("already exists")
324
+ ) {
325
+ console.log(
326
+ `Project '${props.slug || props.name}' already exists, adopting it`,
327
+ );
328
+ // Find the existing project by slug
329
+ const existingProject = await findProjectBySlug(
330
+ api,
331
+ props.organization,
332
+ props.slug || props.name,
333
+ );
334
+ if (!existingProject) {
335
+ throw new Error(
336
+ `Failed to find existing project '${props.slug || props.name}' for adoption`,
337
+ );
338
+ }
339
+ response = await api.get(
340
+ `/projects/${props.organization}/${existingProject.slug}/`,
341
+ );
342
+ } else {
343
+ throw error;
344
+ }
345
+ }
346
+ }
347
+
348
+ if (!response.ok) {
349
+ throw new Error(`API error: ${response.statusText}`);
350
+ }
351
+
352
+ const data = (await response.json()) as Omit<
353
+ Project,
354
+ keyof ProjectProps
355
+ > & { team: Project["team"] };
356
+ return this({
357
+ ...props,
358
+ id: data.id,
359
+ team: data.team,
360
+ teams: data.teams,
361
+ isBookmarked: data.isBookmarked,
362
+ isMember: data.isMember,
363
+ access: data.access,
364
+ hasAccess: data.hasAccess,
365
+ dateCreated: data.dateCreated,
366
+ environments: data.environments,
367
+ eventProcessing: data.eventProcessing,
368
+ features: data.features,
369
+ firstEvent: data.firstEvent,
370
+ firstTransactionEvent: data.firstTransactionEvent,
371
+ hasSessions: data.hasSessions,
372
+ hasProfiles: data.hasProfiles,
373
+ hasReplays: data.hasReplays,
374
+ hasFlags: data.hasFlags,
375
+ hasMonitors: data.hasMonitors,
376
+ hasFeedbacks: data.hasFeedbacks,
377
+ hasNewFeedbacks: data.hasNewFeedbacks,
378
+ hasMinifiedStackTrace: data.hasMinifiedStackTrace,
379
+ hasInsightsHttp: data.hasInsightsHttp,
380
+ hasInsightsDb: data.hasInsightsDb,
381
+ hasInsightsAssets: data.hasInsightsAssets,
382
+ hasInsightsAppStart: data.hasInsightsAppStart,
383
+ hasInsightsScreenLoad: data.hasInsightsScreenLoad,
384
+ hasInsightsVitals: data.hasInsightsVitals,
385
+ hasInsightsCaches: data.hasInsightsCaches,
386
+ hasInsightsQueues: data.hasInsightsQueues,
387
+ hasInsightsLlmMonitoring: data.hasInsightsLlmMonitoring,
388
+ platforms: data.platforms,
389
+ latestRelease: data.latestRelease,
390
+ hasUserReports: data.hasUserReports,
391
+ latestDeploys: data.latestDeploys,
392
+ });
393
+ } catch (error) {
394
+ console.error("Error creating/updating project:", error);
395
+ throw error;
396
+ }
397
+ }
398
+ },
399
+ );
400
+
401
+ /**
402
+ * Find a project by slug
403
+ */
404
+ async function findProjectBySlug(
405
+ api: SentryApi,
406
+ organization: string,
407
+ slug: string,
408
+ ): Promise<{ id: string; slug: string } | null> {
409
+ const response = await api.get(`/projects/${organization}`);
410
+ if (!response.ok) {
411
+ throw new Error(`API error: ${response.statusText}`);
412
+ }
413
+
414
+ const projects = (await response.json()) as Array<{
415
+ id: string;
416
+ slug: string;
417
+ }>;
418
+ const project = projects.find((p) => p.slug === slug);
419
+ return project ? { id: project.id, slug: project.slug } : null;
420
+ }