Merge remote-tracking branch 'origin/main'

This commit is contained in:
Nevo David 2026-03-22 10:10:28 +07:00
commit cb8560e183
16 changed files with 613 additions and 333 deletions

View file

@ -169,10 +169,20 @@ export class AuthController {
}
response.header('reload', 'true');
try {
Sentry.metrics.count('auth.login.success', 1, {
attributes: { provider: body.provider || 'LOCAL' },
} as any);
} catch (e) {}
response.status(200).json({
login: true,
});
} catch (e: any) {
try {
Sentry.metrics.count('auth.login.failure', 1, {
attributes: { 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, { attributes: { provider } } as any);
} catch (e) {}
return response.json({ token });
}

View file

@ -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);
Sentry.metrics.distribution('upload_size_bytes', file?.size || 0);
} catch (e) {}
return this._mediaService.saveFile(
org.id,
uploadedFile.originalname,
uploadedFile.path,
originalName
);
} catch (err) {
try {
Sentry.metrics.count('uploads.failure', 1);
} 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);
Sentry.metrics.distribution('upload_size_bytes', file?.size || 0);
} 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);
} 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);
Sentry.metrics.distribution('upload_size_bytes', (req.headers['content-length'] ? Number(req.headers['content-length']) : 0) || 0);
} catch (e) {}
res.status(200).json({ ...upload, saved: saveFile });
} catch (err) {
try {
Sentry.metrics.count('uploads.failure', 1);
} catch (e) {}
throw err;
}
}
@Get('/')

View file

@ -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, { attributes: { provider } } as any);
}
} catch (e) {}
return created;
}
@Post('/generator/draft')
@ -204,7 +216,11 @@ 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);
return res;
})();
}
@Post('/separate-posts')

View file

@ -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();

View file

@ -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]);

View file

@ -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<AddEditModalProps> = (props) => {
}
if (!dummy) {
try {
Sentry.metrics.count('post.submit', 1, { attributes: { action: type } });
} catch (e) {}
addEditSets
? addEditSets(data)
: await fetch('/posts', {

View file

@ -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', count, { attributes: { taskQueue: post.integration?.providerIdentifier?.split('-')[0] || 'main' } } as any);
} catch (e) {}
} catch (e) {}
}
} catch (e) {}
return res;
}
@ActivityMethod()

View file

@ -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, { attributes: { provider: post.integration?.providerIdentifier } } as any);
Sentry.metrics.distribution('posts.publish_latency_ms', latency, { attributes: { 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, { attributes: { 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, { attributes: { 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, { attributes: { workflow: 'postWorkflowV101' } } as any);
} catch (e) {}
try {
Sentry.metrics.count('posts.published.failure', 1, { attributes: { provider: post.integration?.providerIdentifier, failure_reason: 'retry_exhausted' } } as any);
} catch (e) {}
return false;
}
}

View file

@ -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, any>): string {
const params = new URLSearchParams();
@ -19,6 +21,10 @@ export default class Postiz {
) {}
async post(posts: CreatePostDto) {
try {
Sentry.metrics.count('sdk.requests', 1, { attributes: { method: 'post' } } as any);
} catch (e) {}
return (
await fetch(`${this._path}/public/v1/posts`, {
method: 'POST',
@ -32,6 +38,10 @@ export default class Postiz {
}
async postList(filters: GetPostsDto) {
try {
Sentry.metrics.count('sdk.requests', 1, { attributes: { method: 'list' } } as any);
} catch (e) {}
return (
await fetch(`${this._path}/public/v1/posts?${toQueryString(filters)}`, {
method: 'GET',
@ -44,6 +54,10 @@ export default class Postiz {
}
async upload(file: Buffer, extension: string) {
try {
Sentry.metrics.count('sdk.requests', 1, { attributes: { method: 'upload' } } as any);
} catch (e) {}
const formData = new FormData();
const type =
extension === 'png'
@ -72,6 +86,10 @@ export default class Postiz {
}
async integrations() {
try {
Sentry.metrics.count('sdk.requests', 1, { attributes: { method: 'integrations' } } as any);
} catch (e) {}
return (
await fetch(`${this._path}/public/v1/integrations`, {
method: 'GET',
@ -84,6 +102,10 @@ export default class Postiz {
}
deletePost(id: string) {
try {
Sentry.metrics.count('sdk.requests', 1, { attributes: { method: 'delete' } } as any);
} catch (e) {}
return fetch(`${this._path}/public/v1/posts/${id}`, {
method: 'DELETE',
headers: {

View file

@ -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, { attributes: { 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, { attributes: { media_type: 'image' } } as any);
Sentry.metrics.distribution('media.generation_ms', Date.now() - start, { attributes: { media_type: 'image' } } as any);
} catch (e) {}
return res;
} catch (err) {
try {
Sentry.metrics.count('media.generate_failure', 1, { attributes: { 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, { attributes: { 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, { attributes: { media_type: 'video' } } as any);
Sentry.metrics.distribution('media.generation_ms', Date.now() - start, { attributes: { media_type: 'video' } } as any);
} catch (e) {}
return result;
} catch (err) {
try {
Sentry.metrics.count('media.generate_failure', 1, { attributes: { media_type: 'video' } } as any);
} catch (e) {}
throw err;
}
}
async videoFunction(identifier: string, functionName: string, body: any) {

View file

@ -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', count, { attributes: { 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,

View file

@ -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<AuthTokenDetails | false> {
const refresh: false | AuthTokenDetails = await socialProvider
.refreshToken(integration.refreshToken)
.catch((err) => false);
try {
try {
Sentry.metrics.count('provider.refresh_attempt', 1, { attributes: { 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, { attributes: { 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, { attributes: { 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,
};
}
}

View file

@ -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<Response> {
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, { attributes: { 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, { attributes: { 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, { attributes: { provider: this.identifier } } as any);
} catch (e) {}
await timer(5000);
return this.fetch(
url,

View file

@ -31,6 +31,10 @@ export const initializeSentry = (appName: string, allowLogs = false) => {
recordInputs: true,
recordOutputs: true,
}),
Sentry.langChainIntegration({
recordInputs: true,
recordOutputs: true,
}),
],
tracesSampleRate: 1.0,
enableLogs: true,
@ -42,5 +46,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, { attributes: { service: appName, route: 'unhandledRejection' } } as any);
} catch (e) {}
});
process.on('uncaughtException', (err) => {
try {
Sentry.metrics.count('app.unhandled_errors', 1, { attributes: { service: appName, route: 'uncaughtException' } } as any);
} catch (e) {}
});
} catch (e) {}
return true;
};

View file

@ -80,10 +80,10 @@
"@pigment-css/react": "^0.0.30",
"@postiz/wallets": "^0.0.1",
"@prisma/client": "6.5.0",
"@sentry/nestjs": "^10.26.0",
"@sentry/nextjs": "^10.26.0",
"@sentry/profiling-node": "^10.25.0",
"@sentry/react": "^10.25.0",
"@sentry/nestjs": "^10.45.0",
"@sentry/nextjs": "^10.45.0",
"@sentry/profiling-node": "^10.45.0",
"@sentry/react": "^10.45.0",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/wallet-adapter-react-ui": "^0.9.35",
"@stripe/react-stripe-js": "^5.4.1",

498
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff