feat(pl24): günlük HTTP bütçesi, kullanıcı rezervi ve proxy_logs telemetrisi
Some checks are pending
QA Gate (P0/P1) / Test affected app (pull_request) Waiting to run

Faz 1 / adım 2 (analiz: /home/s/ss/plv2.md, bulgular consumers_jobs-03/06/07,
deploy_ops-13).

SORUN: İki PL24 hesap banı da günde 5-7 bin kategori yazan (≈10 bin+ istek)
otomatik drill dalgalarının ardından geldi; gerçek kullanıcı decode'u ≤14/gün.
Mevcut korumaların hepsi iş-seviyesinde (iş/dk, iş/gün) — HTTP isteği sayan
hiçbir tavan yoktu. Üstelik PL24 çağrıları `proxy_logs`'a hiç yazılmıyordu
(yalnız pcat/emex), bu yüzden hacim ancak ban'dan SONRA, DB satır sayılarından
geriye dönük çıkarılabildi. Kill switch de yalnız decodeVin'i kapatıyordu; drill,
backfill ve katalog gezinmesi "kapalı" kaynağa istek atmaya devam ediyordu.

DEĞİŞİKLİK:
- Yeni `PL24BudgetService`: tüm PL24 upstream trafiği için tek geçit.
  - Günlük sayaç `pl24:http:<gün>` (Redis, 8 gün TTL); tavan `PL24_HTTP_DAILY_MAX`
    (varsayılan 1200).
  - Kullanıcı rezervi `PL24_HTTP_USER_RESERVE` (varsayılan %40): backfill tavanın
    %60'ında durur, kullanıcı decode'u sonuna kadar akar — kaçak bir backfill
    müşteriyi asla aç bırakamaz. Lane ayrımı mevcut `isBackfillContext()` ile.
  - Reddedilen çağrı ağa hiç çıkmaz ve telemetriye yazılmaz.
  - Redis düşerse fail-open (auth/katalog telemetri yüzünden bloklanmaz).
- Telemetri: `ProxyServiceLeg` += `pl24_http`, `pl24_auth`. Login, authorize ve
  tüm katalog fetch'leri `proxy_logs`'a yazıyor (provider dataimpulse/none,
  sessionKey=hesap, status, süre, 403/429 → banned, taşıma hatası sınıflandırma).
  Artık "hangi gün kaç istek attık, ilk 401/403 ne zaman başladı" sorusu
  ban'dan ÖNCE yanıtlanabilir.
- Kill switch yayıldı: `kill-source-pl24` artık `budgetedFetch` içinde, yani
  decode + drill + backfill + browse + görsel indirme dahil TÜM katalog trafiğini
  kapatıyor (eskiden yalnız decodeVin).
- `fetchWithRetry` 401 çaresi netleşti: yalnız servis token'ı tazelenir; oturumun
  kendisi gerekirse auth katmanında düşürülür.

Test: yeni `pl24-budget.service.spec.ts` (9 test: tavan, kullanıcı rezervi,
reddedilen çağrının ağa çıkmaması, Redis fail-open, telemetri biçimi/leg ayrımı,
ban sinyali, taşıma hatası sınıflandırma). Etkilenen paketler: 197 test geçti.
tsc + biome temiz.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
semih
2026-09-17 13:13:05 +03:00
parent e50a907931
commit 56a8ea09be
8 changed files with 420 additions and 8 deletions

View File

@@ -25,6 +25,7 @@
import { Injectable, Logger, type OnModuleInit, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { RedisService } from "../../redis/redis.service";
import { PL24BudgetService } from "./pl24-budget.service";
import { PL24_DEFAULTS, PL24_ENDPOINTS, PL24_USER_AGENT } from "./pl24.constants";
import {
PL24AuthorizeRequest,
@@ -96,6 +97,7 @@ export class PL24AuthService implements OnModuleInit {
constructor(
private configService: ConfigService,
private readonly redis: RedisService,
private readonly budget: PL24BudgetService,
) {
this.baseUrl = this.configService.get<string>("pl24.baseUrl", "https://www.partslink24.com");
this.companyCode = this.configService.get<string>("pl24.companyCode", "");
@@ -405,7 +407,29 @@ export class PL24AuthService implements OnModuleInit {
if (dispatcher) fetchOpts.dispatcher = dispatcher;
const url = `${this.baseUrl}${legacy ? PL24_ENDPOINTS.LOGIN_LEGACY : PL24_ENDPOINTS.LOGIN}`;
const response = await fetch(url, fetchOpts);
await this.budget.consume("auth");
const startedAt = Date.now();
const response = await fetch(url, fetchOpts).catch((err) => {
this.budget.record({
kind: "auth",
url,
proxied: Boolean(dispatcher),
account,
success: false,
startedAt,
error: err,
});
throw err;
});
this.budget.record({
kind: "auth",
url,
proxied: Boolean(dispatcher),
account,
statusCode: response.status,
success: response.ok,
startedAt,
});
const cookieToken = this.extractSessionToken(response);
// 400/412 carry an RFC7807 problem body naming the reason.
@@ -538,7 +562,30 @@ export class PL24AuthService implements OnModuleInit {
};
if (dispatcher) fetchOpts.dispatcher = dispatcher;
const response = await fetch(`${this.baseUrl}${PL24_ENDPOINTS.AUTHORIZE}`, fetchOpts);
const authorizeUrl = `${this.baseUrl}${PL24_ENDPOINTS.AUTHORIZE}`;
await this.budget.consume("auth");
const startedAt = Date.now();
const response = await fetch(authorizeUrl, fetchOpts).catch((err) => {
this.budget.record({
kind: "auth",
url: authorizeUrl,
proxied: Boolean(dispatcher),
account,
success: false,
startedAt,
error: err,
});
throw err;
});
this.budget.record({
kind: "auth",
url: authorizeUrl,
proxied: Boolean(dispatcher),
account,
statusCode: response.status,
success: response.ok,
startedAt,
});
// A dead session shows up here, not at login time.
if (response.status === 401 || response.status === 403) {