From 1c04544c54ed432aa38f8e1cf29ee700e4e9ffe5 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Sat, 21 Mar 2026 22:18:28 +0100 Subject: [PATCH] feat: a lot of sentry metrics --- .../backend/src/api/routes/auth.controller.ts | 13 +++ .../src/api/routes/media.controller.ts | 90 +++++++++++++------ .../src/api/routes/posts.controller.ts | 25 +++++- .../launches/general.preview.component.tsx | 8 +- .../src/components/new-launch/editor.tsx | 7 ++ .../components/new-launch/manage.modal.tsx | 5 ++ .../src/activities/post.activity.ts | 27 +++++- .../post-workflows/post.workflow.v1.0.1.ts | 26 ++++++ apps/sdk/src/index.ts | 37 ++++++++ .../database/prisma/media/media.service.ts | 73 +++++++++++---- .../database/prisma/posts/posts.service.ts | 21 ++++- .../refresh.integration.service.ts | 87 ++++++++++-------- .../src/integrations/social.abstract.ts | 24 ++++- .../src/sentry/initialize.sentry.ts | 13 +++ 14 files changed, 370 insertions(+), 86 deletions(-) diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index 4f87d07d..23ed163e 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -169,10 +169,20 @@ export class AuthController { } response.header('reload', 'true'); + try { + Sentry.metrics.count('auth.login.success', 1, { + tags: { provider: body.provider || 'LOCAL', organizationId: (addedOrg as any)?.organizationId || 'unknown' }, + } as any); + } catch (e) {} response.status(200).json({ login: true, }); } catch (e: any) { + try { + Sentry.metrics.count('auth.login.failure', 1, { + tags: { provider: body?.provider || 'LOCAL' }, + } as any); + } catch (er) {} response.status(400).send(e.message); } } @@ -260,6 +270,9 @@ export class AuthController { const { jwt, token } = await this._authService.checkExists(provider, code); if (token) { + try { + Sentry.metrics.count('oauth.connects', 1, { tags: { provider } } as any); + } catch (e) {} return response.json({ token }); } diff --git a/apps/backend/src/api/routes/media.controller.ts b/apps/backend/src/api/routes/media.controller.ts index 0da66316..f0838c89 100644 --- a/apps/backend/src/api/routes/media.controller.ts +++ b/apps/backend/src/api/routes/media.controller.ts @@ -25,6 +25,7 @@ import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory'; import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/save.media.information.dto'; import { VideoDto } from '@gitroom/nestjs-libraries/dtos/videos/video.dto'; import { VideoFunctionDto } from '@gitroom/nestjs-libraries/dtos/videos/video.function.dto'; +import * as Sentry from '@sentry/nestjs'; @ApiTags('Media') @Controller('/media') @@ -92,13 +93,26 @@ export class MediaController { @UploadedFile() file: Express.Multer.File ) { const originalName = file?.originalname || ''; - const uploadedFile = await this.storage.uploadFile(file); - return this._mediaService.saveFile( - org.id, - uploadedFile.originalname, - uploadedFile.path, - originalName - ); + try { + const uploadedFile = await this.storage.uploadFile(file); + + try { + Sentry.metrics.count('uploads.total', 1, { tags: { organizationId: org.id, storage_backend: process.env.STORAGE_PROVIDER || 'local' } } as any); + Sentry.metrics.distribution('upload_size_bytes', file?.size || 0, { tags: { organizationId: org.id, storage_backend: process.env.STORAGE_PROVIDER || 'local' } } as any); + } catch (e) {} + + return this._mediaService.saveFile( + org.id, + uploadedFile.originalname, + uploadedFile.path, + originalName + ); + } catch (err) { + try { + Sentry.metrics.count('uploads.failure', 1, { tags: { organizationId: org.id, storage_backend: process.env.STORAGE_PROVIDER || 'local' } } as any); + } catch (e) {} + throw err; + } } @Post('/save-media') @@ -135,19 +149,31 @@ export class MediaController { @Body('preventSave') preventSave: string = 'false' ) { const originalName = file.originalname; - const getFile = await this.storage.uploadFile(file); + try { + const getFile = await this.storage.uploadFile(file); - if (preventSave === 'true') { - const { path } = getFile; - return { path }; + try { + Sentry.metrics.count('uploads.total', 1, { tags: { organizationId: org.id, storage_backend: process.env.STORAGE_PROVIDER || 'local' } } as any); + Sentry.metrics.distribution('upload_size_bytes', file?.size || 0, { tags: { organizationId: org.id, storage_backend: process.env.STORAGE_PROVIDER || 'local' } } as any); + } catch (e) {} + + if (preventSave === 'true') { + const { path } = getFile; + return { path }; + } + + return this._mediaService.saveFile( + org.id, + getFile.originalname, + getFile.path, + originalName + ); + } catch (err) { + try { + Sentry.metrics.count('uploads.failure', 1, { tags: { organizationId: org.id, storage_backend: process.env.STORAGE_PROVIDER || 'local' } } as any); + } catch (e) {} + throw err; } - - return this._mediaService.saveFile( - org.id, - getFile.originalname, - getFile.path, - originalName - ); } @Post('/:endpoint') @@ -166,15 +192,27 @@ export class MediaController { const name = upload.Location.split('/').pop(); const originalName = req.body?.file?.name; - const saveFile = await this._mediaService.saveFile( - org.id, - name, - // @ts-ignore - upload.Location, - originalName || undefined - ); + try { + const saveFile = await this._mediaService.saveFile( + org.id, + name, + // @ts-ignore + upload.Location, + originalName || undefined + ); - res.status(200).json({ ...upload, saved: saveFile }); + try { + Sentry.metrics.count('uploads.total', 1, { tags: { organizationId: org.id, storage_backend: process.env.STORAGE_PROVIDER || 'local' } } as any); + Sentry.metrics.distribution('upload_size_bytes', (req.headers['content-length'] ? Number(req.headers['content-length']) : 0) || 0, { tags: { organizationId: org.id, storage_backend: process.env.STORAGE_PROVIDER || 'local' } } as any); + } catch (e) {} + + res.status(200).json({ ...upload, saved: saveFile }); + } catch (err) { + try { + Sentry.metrics.count('uploads.failure', 1, { tags: { organizationId: org.id, storage_backend: process.env.STORAGE_PROVIDER || 'local' } } as any); + } catch (e) {} + throw err; + } } @Get('/') diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts index 84b1b748..4a17785e 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -16,6 +16,7 @@ import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto' import { GetPostsListDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.list.dto'; import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; import { ApiTags } from '@nestjs/swagger'; +import * as Sentry from '@sentry/nestjs'; import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto'; import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto'; import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service'; @@ -162,7 +163,18 @@ export class PostsController { ) { console.log(JSON.stringify(rawBody, null, 2)); const body = await this._postsService.mapTypeToPost(rawBody, org.id); - return this._postsService.createPost(org.id, body); + const created = await this._postsService.createPost(org.id, body); + + try { + for (const p of body.posts || []) { + const providerRaw = (p?.settings && p.settings.__type) || (p?.integration && p.integration.id) || ''; + const provider = (typeof providerRaw === 'string' ? providerRaw.split('-')[0] : '') + .toLowerCase(); + Sentry.metrics.count('posts.created', 1, { tags: { organizationId: org.id, provider } } as any); + } + } catch (e) {} + + return created; } @Post('/generator/draft') @@ -204,7 +216,16 @@ export class PostsController { @Body('date') date: string, @Body('action') action: 'schedule' | 'update' = 'schedule' ) { - return this._postsService.changeDate(org.id, id, date, action); + return (async () => { + const res = await this._postsService.changeDate(org.id, id, date, action); + if (action === 'schedule') { + try { + Sentry.metrics.count('posts.scheduled', 1, { tags: { organizationId: org.id, scheduleType: action } } as any); + } catch (e) {} + } + + return res; + })(); } @Post('/separate-posts') diff --git a/apps/frontend/src/components/launches/general.preview.component.tsx b/apps/frontend/src/components/launches/general.preview.component.tsx index 6682b97c..5ce3ba3c 100644 --- a/apps/frontend/src/components/launches/general.preview.component.tsx +++ b/apps/frontend/src/components/launches/general.preview.component.tsx @@ -2,7 +2,8 @@ import { useIntegration } from '@gitroom/frontend/components/launches/helpers/us import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory'; import clsx from 'clsx'; import { VideoOrImage } from '@gitroom/react/helpers/video.or.image'; -import { FC } from 'react'; +import { FC, useEffect } from 'react'; +import * as Sentry from '@sentry/nextjs'; import { textSlicer } from '@gitroom/helpers/utils/count.length'; import Image from 'next/image'; import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; @@ -11,6 +12,11 @@ import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validatio export const GeneralPreviewComponent: FC<{ maximumCharacters?: number; }> = (props) => { + useEffect(() => { + try { + Sentry.metrics.count('preview.render', 1); + } catch (e) {} + }, []); const { value: topValue, integration } = useIntegration(); const current = useLaunchStore((state) => state.current); const mediaDir = useMediaDirectory(); diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 511f25d8..84e42719 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -11,6 +11,7 @@ import React, { forwardRef, useImperativeHandle, } from 'react'; +import * as Sentry from '@sentry/nextjs'; import clsx from 'clsx'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import EmojiPicker from 'emoji-picker-react'; @@ -183,6 +184,12 @@ export const EditorWrapper: FC<{ setLoaded(true); }, [loaded, loadedState]); + useEffect(() => { + try { + Sentry.metrics.count('editor.open', 1); + } catch (e) {} + }, []); + const canEdit = useMemo(() => { return current === 'global' || !!internal; }, [current, internal]); diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index 243da51f..196e90ad 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -33,6 +33,7 @@ import { SelectCustomer } from '@gitroom/frontend/components/launches/select.cus import { CopilotPopup } from '@copilotkit/react-ui'; import { DummyCodeComponent } from '@gitroom/frontend/components/new-launch/dummy.code.component'; import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation'; +import * as Sentry from '@sentry/nextjs'; import { SettingsIcon, ChevronDownIcon, @@ -413,6 +414,10 @@ export const ManageModal: FC = (props) => { } if (!dummy) { + try { + Sentry.metrics.count('post.submit', 1, { attributes: { action: type } }); + } catch (e) {} + addEditSets ? addEditSets(data) : await fetch('/posts', { diff --git a/apps/orchestrator/src/activities/post.activity.ts b/apps/orchestrator/src/activities/post.activity.ts index 5467d353..78e66224 100644 --- a/apps/orchestrator/src/activities/post.activity.ts +++ b/apps/orchestrator/src/activities/post.activity.ts @@ -23,6 +23,7 @@ import { postId as postIdSearchParam, } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; +import * as Sentry from '@sentry/nestjs'; @Injectable() @Activity() @@ -80,7 +81,31 @@ export class PostActivity { @ActivityMethod() async updatePost(id: string, postId: string, releaseURL: string) { - return this._postService.updatePost(id, postId, releaseURL); + const res = await this._postService.updatePost(id, postId, releaseURL); + try { + const posts = await this._postService.getPostByForWebhookId(postId); + const post = Array.isArray(posts) && posts.length ? (posts[0] as any) : (posts as any); + if (post && post.organizationId) { + try { + const running = this._temporalService.client + .getRawClient() + ?.workflow.list({ query: `organizationId="${post.organizationId}" AND ExecutionStatus="Running"` }); + + let count = 0; + if (running) { + for await (const _ of running) { + count++; + } + } + + try { + Sentry.metrics.gauge('posts.queued_per_org', count, { tags: { organizationId: post.organizationId, taskQueue: post.integration?.providerIdentifier?.split('-')[0] || 'main' } } as any); + } catch (e) {} + } catch (e) {} + } + } catch (e) {} + + return res; } @ActivityMethod() diff --git a/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.0.1.ts b/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.0.1.ts index b8fce6ac..76416950 100644 --- a/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.0.1.ts +++ b/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.0.1.ts @@ -1,4 +1,5 @@ import { PostActivity } from '@gitroom/orchestrator/activities/post.activity'; +import * as Sentry from '@sentry/node'; import { ActivityFailure, ApplicationFailure, @@ -171,6 +172,14 @@ export async function postWorkflowV101({ postsResults[i].releaseURL ); + if (i === 0) { + try { + const latency = Date.now() - startTime.getTime(); + Sentry.metrics.count('posts.published.success', 1, { tags: { organizationId: post.organizationId, provider: post.integration?.providerIdentifier } } as any); + Sentry.metrics.distribution('posts.publish_latency_ms', latency, { tags: { organizationId: post.organizationId, provider: post.integration?.providerIdentifier } } as any); + } catch (e) {} + } + if (i === 0) { // send notification on a sucessful post await inAppNotification( @@ -207,6 +216,16 @@ export async function postWorkflowV101({ // for other errors, change state and inform the user if needed await changeState(postsList[0].id, 'ERROR', err, postsList); + try { + const cause = (err as any)?.cause; + const failure_reason = (cause && (cause as any).type) || (err as any)?.message || 'unknown'; + Sentry.metrics.count('posts.published.failure', 1, { tags: { organizationId: post.organizationId, provider: post.integration?.providerIdentifier, failure_reason } } as any); + } catch (e) {} + try { + const cause = (err as any)?.cause; + const reason = (cause && (cause as any).type) || (err as any)?.message || 'unknown'; + Sentry.metrics.count('task_failures_by_reason', 1, { tags: { reason } } as any); + } catch (e) {} // specific case for bad body errors if ( @@ -233,6 +252,13 @@ export async function postWorkflowV101({ if (postsResults.length === before) { // all retries exhausted without success + try { + Sentry.metrics.count('temporal.retry_exhausted', 1, { tags: { workflow: 'postWorkflowV101', organizationId: post.organizationId } } as any); + } catch (e) {} + try { + Sentry.metrics.count('posts.published.failure', 1, { tags: { organizationId: post.organizationId, provider: post.integration?.providerIdentifier, failure_reason: 'retry_exhausted' } } as any); + } catch (e) {} + return false; } } diff --git a/apps/sdk/src/index.ts b/apps/sdk/src/index.ts index d92d0d0c..e6cd69d4 100644 --- a/apps/sdk/src/index.ts +++ b/apps/sdk/src/index.ts @@ -1,6 +1,8 @@ import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto'; import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto'; import fetch, { FormData } from 'node-fetch'; +import * as Sentry from '@sentry/node'; +import crypto from 'crypto'; function toQueryString(obj: Record): string { const params = new URLSearchParams(); @@ -19,6 +21,13 @@ export default class Postiz { ) {} async post(posts: CreatePostDto) { + try { + const apiHash = crypto.createHash('sha256').update(this._apiKey).digest('hex').slice(0, 8); + try { + Sentry.metrics.count('sdk.requests', 1, { tags: { method: 'post', api_key_hash: apiHash } } as any); + } catch (e) {} + } catch (e) {} + return ( await fetch(`${this._path}/public/v1/posts`, { method: 'POST', @@ -32,6 +41,13 @@ export default class Postiz { } async postList(filters: GetPostsDto) { + try { + const apiHash = crypto.createHash('sha256').update(this._apiKey).digest('hex').slice(0, 8); + try { + Sentry.metrics.count('sdk.requests', 1, { tags: { method: 'list', api_key_hash: apiHash } } as any); + } catch (e) {} + } catch (e) {} + return ( await fetch(`${this._path}/public/v1/posts?${toQueryString(filters)}`, { method: 'GET', @@ -44,6 +60,13 @@ export default class Postiz { } async upload(file: Buffer, extension: string) { + try { + const apiHash = crypto.createHash('sha256').update(this._apiKey).digest('hex').slice(0, 8); + try { + Sentry.metrics.count('sdk.requests', 1, { tags: { method: 'upload', api_key_hash: apiHash } } as any); + } catch (e) {} + } catch (e) {} + const formData = new FormData(); const type = extension === 'png' @@ -72,6 +95,13 @@ export default class Postiz { } async integrations() { + try { + const apiHash = crypto.createHash('sha256').update(this._apiKey).digest('hex').slice(0, 8); + try { + Sentry.metrics.count('sdk.requests', 1, { tags: { method: 'integrations', api_key_hash: apiHash } } as any); + } catch (e) {} + } catch (e) {} + return ( await fetch(`${this._path}/public/v1/integrations`, { method: 'GET', @@ -84,6 +114,13 @@ export default class Postiz { } deletePost(id: string) { + try { + const apiHash = crypto.createHash('sha256').update(this._apiKey).digest('hex').slice(0, 8); + try { + Sentry.metrics.count('sdk.requests', 1, { tags: { method: 'delete', api_key_hash: apiHash } } as any); + } catch (e) {} + } catch (e) {} + return fetch(`${this._path}/public/v1/posts/${id}`, { method: 'DELETE', headers: { diff --git a/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts b/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts index 66bdac84..26bbca97 100644 --- a/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts @@ -7,6 +7,7 @@ import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/sa import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager'; import { VideoDto } from '@gitroom/nestjs-libraries/dtos/videos/video.dto'; import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory'; +import * as Sentry from '@sentry/nestjs'; import { AuthorizationActions, Sections, @@ -41,11 +42,31 @@ export class MediaService { org, 'ai_images', async () => { - if (generatePromptFirst) { - prompt = await this._openAi.generatePromptForPicture(prompt); - console.log('Prompt:', prompt); + const start = Date.now(); + try { + try { + Sentry.metrics.count('media.generate_attempt', 1, { tags: { organizationId: org.id, media_type: 'image' } } as any); + } catch (e) {} + + if (generatePromptFirst) { + prompt = await this._openAi.generatePromptForPicture(prompt); + console.log('Prompt:', prompt); + } + + const res = await this._openAi.generateImage(prompt, !!generatePromptFirst); + + try { + Sentry.metrics.count('media.generate_success', 1, { tags: { organizationId: org.id, media_type: 'image' } } as any); + Sentry.metrics.distribution('media.generation_ms', Date.now() - start, { tags: { organizationId: org.id, media_type: 'image' } } as any); + } catch (e) {} + + return res; + } catch (err) { + try { + Sentry.metrics.count('media.generate_failure', 1, { tags: { organizationId: org.id, media_type: 'image' } } as any); + } catch (e) {} + throw err; } - return this._openAi.generateImage(prompt, !!generatePromptFirst); } ); @@ -105,21 +126,39 @@ export class MediaService { console.log(body.customParams); await video.instance.processAndValidate(body.customParams); - console.log('no err'); - return await this._subscriptionService.useCredit( - org, - 'ai_videos', - async () => { - const loadedData = await video.instance.process( - body.output, - body.customParams - ); + const start = Date.now(); + try { + try { + Sentry.metrics.count('media.generate_attempt', 1, { tags: { organizationId: org.id, media_type: 'video' } } as any); + } catch (e) {} - const file = await this.storage.uploadSimple(loadedData); - return this.saveFile(org.id, file.split('/').pop(), file); - } - ); + const result = await this._subscriptionService.useCredit( + org, + 'ai_videos', + async () => { + const loadedData = await video.instance.process( + body.output, + body.customParams + ); + + const file = await this.storage.uploadSimple(loadedData); + return this.saveFile(org.id, file.split('/').pop(), file); + } + ); + + try { + Sentry.metrics.count('media.generate_success', 1, { tags: { organizationId: org.id, media_type: 'video' } } as any); + Sentry.metrics.distribution('media.generation_ms', Date.now() - start, { tags: { organizationId: org.id, media_type: 'video' } } as any); + } catch (e) {} + + return result; + } catch (err) { + try { + Sentry.metrics.count('media.generate_failure', 1, { tags: { organizationId: org.id, media_type: 'video' } } as any); + } catch (e) {} + throw err; + } } async videoFunction(identifier: string, functionName: string, body: any) { diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 54d72355..4cb6d584 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -681,6 +681,25 @@ export class PostsService { }, ]), }); + // update queued-per-org gauge (simple approach: count running workflows for org) + try { + const running = this._temporalService.client + .getRawClient() + ?.workflow.list({ + query: `organizationId="${orgId}" AND ExecutionStatus="Running"`, + }); + + let count = 0; + if (running) { + for await (const _ of running) { + count++; + } + } + + try { + Sentry.metrics.gauge('posts.queued_per_org', count, { tags: { organizationId: orgId, taskQueue } } as any); + } catch (err) {} + } catch (err) {} } catch (err) {} } @@ -719,7 +738,7 @@ export class PostsService { ).catch((err) => {}); } - Sentry.metrics.count('post_created', 1); + // metric moved: controller records `posts.created` with org/provider tags postList.push({ postId: posts[0].id, integration: post.integration.id, diff --git a/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts b/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts index a14d990b..defd99f5 100644 --- a/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts +++ b/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts @@ -7,6 +7,7 @@ import { SocialProvider, } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { TemporalService } from 'nestjs-temporal-core'; +import * as Sentry from '@sentry/nestjs'; @Injectable() export class RefreshIntegrationService { @@ -71,45 +72,61 @@ export class RefreshIntegrationService { integration: Integration, socialProvider: SocialProvider ): Promise { - const refresh: false | AuthTokenDetails = await socialProvider - .refreshToken(integration.refreshToken) - .catch((err) => false); + try { + try { + Sentry.metrics.count('provider.refresh_attempt', 1, { tags: { provider: socialProvider.identifier } } as any); + } catch (e) {} - if (!refresh || !refresh.accessToken) { - await this._integrationService.refreshNeeded( - integration.organizationId, - integration.id - ); - - await this._integrationService.informAboutRefreshError( - integration.organizationId, - integration - ); - - await this._integrationService.disconnectChannel( - integration.organizationId, - integration + const refresh: false | AuthTokenDetails = await socialProvider + .refreshToken(integration.refreshToken) + .catch((err) => false); + + if (!refresh || !refresh.accessToken) { + try { + Sentry.metrics.count('provider.refresh_fail', 1, { tags: { provider: socialProvider.identifier } } as any); + } catch (e) {} + + await this._integrationService.refreshNeeded( + integration.organizationId, + integration.id + ); + + await this._integrationService.informAboutRefreshError( + integration.organizationId, + integration + ); + + await this._integrationService.disconnectChannel( + integration.organizationId, + integration + ); + + return false; + } + + // proceed with reconnect handling below + if ( + !socialProvider.reConnect || + integration.rootInternalId === integration.internalId + ) { + return refresh; + } + + const reConnect = await socialProvider.reConnect( + integration.rootInternalId, + integration.internalId, + refresh.accessToken ); + return { + ...refresh, + ...reConnect, + }; + } catch (err) { + try { + Sentry.metrics.count('provider.refresh_fail', 1, { tags: { provider: socialProvider.identifier } } as any); + } catch (e) {} return false; } - - if ( - !socialProvider.reConnect || - integration.rootInternalId === integration.internalId - ) { - return refresh; - } - - const reConnect = await socialProvider.reConnect( - integration.rootInternalId, - integration.internalId, - refresh.accessToken - ); - - return { - ...refresh, - ...reConnect, - }; } } diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index ec13f7f6..18348006 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -1,6 +1,7 @@ import { timer } from '@gitroom/helpers/utils/timer'; import { Integration } from '@prisma/client'; import { ApplicationFailure } from '@temporalio/activity'; +import * as Sentry from '@sentry/nestjs'; export class RefreshToken extends ApplicationFailure { constructor(identifier: string, json: string, body: BodyInit, message = '') { @@ -104,10 +105,24 @@ export abstract class SocialAbstract { totalRetries = 0, ignoreConcurrency = false ): Promise { - const request = await fetch(url, options); + const start = Date.now(); + let request: Response; + try { + request = await fetch(url, options); + const latency = Date.now() - start; + try { + Sentry.metrics.distribution('provider.api_latency_ms', latency, { tags: { provider: this.identifier, endpoint: url, status: 'success' } } as any); + } catch (e) {} - if (request.status === 200 || request.status === 201) { - return request; + if (request.status === 200 || request.status === 201) { + return request; + } + } catch (err) { + const latency = Date.now() - start; + try { + Sentry.metrics.distribution('provider.api_latency_ms', latency, { tags: { provider: this.identifier, endpoint: url, status: 'failure' } } as any); + } catch (e) {} + throw err; } if (totalRetries > 2) { @@ -129,6 +144,9 @@ export abstract class SocialAbstract { json.includes('rate_limit_exceeded') || json.includes('Rate limit') ) { + try { + Sentry.metrics.count('provider.rate_limited', 1, { tags: { provider: this.identifier } } as any); + } catch (e) {} await timer(5000); return this.fetch( url, diff --git a/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts b/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts index b92c6627..ed37aa0b 100644 --- a/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts +++ b/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts @@ -42,5 +42,18 @@ export const initializeSentry = (appName: string, allowLogs = false) => { } catch (err) { console.log(err); } + try { + process.on('unhandledRejection', (reason) => { + try { + Sentry.metrics.count('app.unhandled_errors', 1, { tags: { service: appName, route: 'unhandledRejection' } } as any); + } catch (e) {} + }); + + process.on('uncaughtException', (err) => { + try { + Sentry.metrics.count('app.unhandled_errors', 1, { tags: { service: appName, route: 'uncaughtException' } } as any); + } catch (e) {} + }); + } catch (e) {} return true; };