chore: add mobile postman updates and feed timing logs
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled

هذا الالتزام موجود في:
boutmoun123
2026-06-07 00:16:43 +03:00
الأصل f149b77139
التزام db8628dc68
4 ملفات معدلة مع 165 إضافات و9 حذوفات

عرض الملف

@@ -5403,6 +5403,28 @@
}
}
]
},
{
"name": "Public Get Shop By Admin Id",
"request": {
"method": "GET",
"header": [],
"url": "{{baseUrl}}/marketplace/shops/{{adminUserId}}",
"description": "Fetch the public marketplace shop profile for a specific admin/store owner."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 200', function () { pm.response.to.have.status(200); });",
"const json = pm.response.json();",
"pm.expect(json).to.be.an('object');"
]
}
}
]
}
]
}
@@ -5827,10 +5849,16 @@
"name": "Register Device FCM Token",
"request": {
"method": "POST",
"header": {
"header": [
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
},
{
"key": "Content-Type",
"value": "application/json"
},
}
],
"url": "{{baseUrl}}/devices/register",
"body": {
"mode": "raw",
@@ -5851,10 +5879,16 @@
"name": "Unregister Device FCM Token",
"request": {
"method": "POST",
"header": {
"header": [
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
},
{
"key": "Content-Type",
"value": "application/json"
},
}
],
"url": "{{baseUrl}}/devices/unregister",
"body": {
"mode": "raw",
@@ -6401,6 +6435,21 @@
{
"key": "collaborationAttachmentType",
"value": "audio"
},
{
"key": "collaborationRequestId",
"value": "",
"type": "string"
},
{
"key": "fcmToken",
"value": "postman-fcm-token",
"type": "string"
},
{
"key": "deviceId",
"value": "postman-device-android",
"type": "string"
}
]
}

عرض الملف

@@ -135,6 +135,10 @@ export default () => ({
userFeedTtlSeconds: Number(process.env.FEED_CACHE_USER_TTL_SECONDS ?? 15),
trendingTtlSeconds: Number(process.env.FEED_CACHE_TRENDING_TTL_SECONDS ?? 30),
},
performance: {
feedTimingLogsEnabled:
(process.env.FEED_TIMING_LOGS_ENABLED ?? 'false').toLowerCase() === 'true',
},
passwordReset: {
codeExpiresMinutes: Number(process.env.PASSWORD_RESET_CODE_EXPIRES_MINUTES ?? 10),
maxAttempts: Number(process.env.PASSWORD_RESET_MAX_ATTEMPTS ?? 5),

عرض الملف

@@ -88,6 +88,7 @@ export const validationSchema = Joi.object({
FEED_CACHE_ENABLED: Joi.boolean().truthy('true').falsy('false').default(true),
FEED_CACHE_USER_TTL_SECONDS: Joi.number().min(1).max(3600).default(15),
FEED_CACHE_TRENDING_TTL_SECONDS: Joi.number().min(1).max(3600).default(30),
FEED_TIMING_LOGS_ENABLED: Joi.boolean().truthy('true').falsy('false').default(false),
PASSWORD_RESET_CODE_EXPIRES_MINUTES: Joi.number().min(1).max(60).default(10),
PASSWORD_RESET_MAX_ATTEMPTS: Joi.number().min(1).max(10).default(5),
PASSWORD_RESET_TOKEN_SECRET: Joi.string().allow('').optional(),

عرض الملف

@@ -1,6 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Types } from 'mongoose';
import { performance } from 'perf_hooks';
import { PostType } from '../../common/enums/post-type.enum';
import { PostVisibility } from '../../common/enums/post-visibility.enum';
import { decodeOffsetCursor, encodeOffsetCursor } from '../../common/utils/cursor.util';
@@ -57,6 +58,8 @@ type FeedCardItem =
@Injectable()
export class FeedService {
private readonly logger = new Logger(FeedService.name);
constructor(
private readonly feedRepository: FeedRepository,
private readonly usersRepository: UsersRepository,
@@ -71,6 +74,7 @@ export class FeedService {
) {}
async getMyFeed(currentUserId: string, query: FeedQueryDto) {
const timing = this.startTiming('feed.me');
const followingOnly = query.followingOnly ?? true;
const cacheEnabled =
this.configService.get<boolean>('feedCache.enabled', { infer: true }) ?? true;
@@ -91,6 +95,12 @@ export class FeedService {
if (cacheEnabled) {
const cached = await this.cacheService.get<Record<string, unknown>>(cacheKey);
if (cached) {
this.logFeedTiming(timing, {
cacheHit: true,
itemCount: Array.isArray(cached.items) ? cached.items.length : undefined,
nextCursor: cached.nextCursor ?? null,
responseBytes: this.measureResponseBytes(cached),
});
return cached;
}
}
@@ -110,8 +120,10 @@ export class FeedService {
this.feedRepository.findFollowingIds(currentUserId),
this.blocksService.getInvisibleUserIds(currentUserId),
]);
const relationLookupMs = this.markTiming(timing);
let filter = this.buildVisiblePostsFilter(currentUserId, followingIds, followingOnly, invisibleUserIds);
let candidates = await this.feedRepository.findCandidatePosts(filter, Math.max(limit * 12, 300));
const firstCandidateLookupMs = this.markTiming(timing);
// Keep the default home feed focused on followed accounts, but avoid an empty screen
// for new users or when followed accounts have not posted yet.
@@ -123,6 +135,7 @@ export class FeedService {
filter = this.buildVisiblePostsFilter(currentUserId, followingIds, false, invisibleUserIds);
candidates = await this.feedRepository.findCandidatePosts(filter, Math.max(limit * 12, 300));
}
const fallbackCandidateLookupMs = this.markTiming(timing);
const scored = candidates
.filter((post) => {
@@ -148,6 +161,7 @@ export class FeedService {
new Date((b.post as any).createdAt ?? 0).getTime() -
new Date((a.post as any).createdAt ?? 0).getTime(),
);
const scoringMs = this.markTiming(timing);
const total = scored.length;
const pagedPosts = scored.slice(skip, skip + limit).map((entry) => ({
@@ -155,9 +169,11 @@ export class FeedService {
feedScore: Number(entry.score.toFixed(3)),
}));
const decoratedPosts = await this.decoratePostsForViewer(currentUserId, pagedPosts, followingIds);
const decorationMs = this.markTiming(timing);
const items = includeSuggestions
? await this.mixHomeFeedItems(currentUserId, decoratedPosts, query.suggestionInterval ?? 4)
: decoratedPosts;
const cardsMs = this.markTiming(timing);
const nextOffset = skip + pagedPosts.length;
const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null;
@@ -179,6 +195,29 @@ export class FeedService {
);
}
this.logFeedTiming(timing, {
cacheHit: false,
page,
limit,
skip,
followingOnly,
includeSuggestions,
followingCount: followingIds.length,
invisibleUserCount: invisibleUserIds.length,
candidateCount: candidates.length,
scoredCount: scored.length,
itemCount: items.length,
postItemCount: decoratedPosts.length,
nextCursor,
relationLookupMs,
firstCandidateLookupMs,
fallbackCandidateLookupMs,
scoringMs,
decorationMs,
cardsMs,
responseBytes: this.measureResponseBytes(result),
});
return result;
}
@@ -191,6 +230,7 @@ export class FeedService {
}
async getTrending(currentUserId: string, query: FeedQueryDto) {
const timing = this.startTiming('feed.trending');
const cacheEnabled =
this.configService.get<boolean>('feedCache.enabled', { infer: true }) ?? true;
const globalVersion = cacheEnabled ? await this.feedVersionService.getGlobalVersion() : 0;
@@ -205,6 +245,12 @@ export class FeedService {
if (cacheEnabled) {
const cached = await this.cacheService.get<Record<string, unknown>>(cacheKey);
if (cached) {
this.logFeedTiming(timing, {
cacheHit: true,
itemCount: Array.isArray(cached.items) ? cached.items.length : undefined,
nextCursor: cached.nextCursor ?? null,
responseBytes: this.measureResponseBytes(cached),
});
return cached;
}
}
@@ -217,6 +263,7 @@ export class FeedService {
this.feedRepository.findFollowingIds(currentUserId),
this.blocksService.getInvisibleUserIds(currentUserId),
]);
const relationLookupMs = this.markTiming(timing);
const trendingFilter: Record<string, unknown> = { visibility: PostVisibility.PUBLIC };
if (invisibleUserIds.length) {
trendingFilter.authorId = { $nin: invisibleUserIds.map((id) => new Types.ObjectId(id)) };
@@ -229,11 +276,13 @@ export class FeedService {
this.feedRepository.findTrendingPublicPosts(trendingFilter, skip, limit),
this.feedRepository.count(trendingFilter),
]);
const postLookupMs = this.markTiming(timing);
const decoratedPosts = await this.decoratePostsForViewer(
currentUserId,
rows.map((item) => item.toObject() as unknown as Record<string, unknown>),
followingIds,
);
const decorationMs = this.markTiming(timing);
const nextOffset = skip + rows.length;
const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null;
@@ -255,6 +304,23 @@ export class FeedService {
);
}
this.logFeedTiming(timing, {
cacheHit: false,
page,
limit,
skip,
preferredPostType: query.preferredPostType ?? '',
followingCount: followingIds.length,
invisibleUserCount: invisibleUserIds.length,
itemCount: decoratedPosts.length,
total,
nextCursor,
relationLookupMs,
postLookupMs,
decorationMs,
responseBytes: this.measureResponseBytes(result),
});
return result;
}
@@ -581,4 +647,40 @@ export class FeedService {
return '';
}
private startTiming(scope: string): { scope: string; startedAt: number; lastAt: number } {
const now = performance.now();
return { scope, startedAt: now, lastAt: now };
}
private markTiming(timing: { lastAt: number }): number {
const now = performance.now();
const elapsed = now - timing.lastAt;
timing.lastAt = now;
return Math.round(elapsed);
}
private logFeedTiming(
timing: { scope: string; startedAt: number },
details: Record<string, unknown>,
): void {
if (!this.configService.get<boolean>('performance.feedTimingLogsEnabled', { infer: true })) {
return;
}
this.logger.log({
event: 'feed_timing',
scope: timing.scope,
totalMs: Math.round(performance.now() - timing.startedAt),
...details,
});
}
private measureResponseBytes(value: unknown): number {
try {
return Buffer.byteLength(JSON.stringify(value), 'utf8');
} catch {
return 0;
}
}
}