@kaikybrofc/omnizap-system 2.2.5 → 2.2.7

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.
Files changed (33) hide show
  1. package/.prettierrc +16 -0
  2. package/README.md +11 -11
  3. package/app/modules/stickerPackModule/autoPackCollectorService.js +63 -8
  4. package/app/observability/metrics.js +15 -144
  5. package/index.js +13 -13
  6. package/kaikybrofc-omnizap-system-2.2.7.tgz +0 -0
  7. package/observability/sticker-catalog-slo.md +83 -0
  8. package/observability/sticker-scale-hardening-rollout.md +128 -0
  9. package/package.json +3 -35
  10. package/public/assets/images/brand-icon-192.png +0 -0
  11. package/public/assets/images/brand-logo-128.webp +0 -0
  12. package/public/assets/images/hero-banner-1280.avif +0 -0
  13. package/public/assets/images/hero-banner-1280.jpg +0 -0
  14. package/public/assets/images/hero-banner-1280.webp +0 -0
  15. package/public/assets/images/hero-banner-720.avif +0 -0
  16. package/public/assets/images/hero-banner-720.webp +0 -0
  17. package/public/index.html +91 -16
  18. package/public/js/apps/homeApp.js +469 -353
  19. package/public/robots.txt +9 -0
  20. package/public/sitemap.xml +28 -0
  21. package/server/controllers/metricsController.js +21 -0
  22. package/{app/modules/stickerPackModule/stickerPackCatalogHttp.js → server/controllers/stickerCatalogController.js} +66 -34
  23. package/server/http/httpServer.js +121 -0
  24. package/server/http/requestContext.js +20 -0
  25. package/server/index.js +1 -0
  26. package/server/routes/metricsRoute.js +7 -0
  27. package/server/routes/stickerCatalogRoute.js +20 -0
  28. package/observability/mysql-exporter.cnf +0 -5
  29. /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogHandlers/catalogAdminHttp.js +0 -0
  30. /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogHandlers/catalogAuthHttp.js +0 -0
  31. /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogHandlers/catalogPublicHttp.js +0 -0
  32. /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogHandlers/catalogUploadHttp.js +0 -0
  33. /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogRouter.js +0 -0
package/.prettierrc ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "printWidth": 12000,
3
+ "tabWidth": 2,
4
+ "useTabs": false,
5
+ "semi": true,
6
+ "singleQuote": true,
7
+ "trailingComma": "all",
8
+ "bracketSpacing": true,
9
+ "arrowParens": "always",
10
+ "endOfLine": "lf",
11
+ "bracketSameLine": false,
12
+ "proseWrap": "preserve",
13
+ "htmlWhitespaceSensitivity": "css",
14
+ "quoteProps": "as-needed",
15
+ "embeddedLanguageFormatting": "auto"
16
+ }
package/README.md CHANGED
@@ -53,26 +53,26 @@ Atualização em cache: **30 minutos** por padrão (`README_SUMMARY_CACHE_SECOND
53
53
  <!-- README_SNAPSHOT:START -->
54
54
  ### Snapshot do Sistema
55
55
 
56
- > Atualizado em `2026-02-28T04:28:50.529Z` | cache `1800s`
56
+ > Atualizado em `2026-02-28T09:29:12.922Z` | cache `1800s`
57
57
 
58
58
  | Métrica | Valor |
59
59
  | --- | ---: |
60
60
  | Usuários (lid_map) | 5.504 |
61
61
  | Grupos | 116 |
62
- | Packs | 294 |
63
- | Stickers | 6.796 |
64
- | Mensagens registradas | 440.582 |
62
+ | Packs | 295 |
63
+ | Stickers | 6.811 |
64
+ | Mensagens registradas | 441.124 |
65
65
 
66
66
  #### Tipos de mensagem mais usados (amostra: 25.000)
67
67
  | Tipo | Total |
68
68
  | --- | ---: |
69
- | `texto` | 16.293 |
70
- | `figurinha` | 4.718 |
71
- | `reacao` | 1.506 |
72
- | `imagem` | 1.286 |
73
- | `outros` | 744 |
74
- | `video` | 232 |
75
- | `audio` | 216 |
69
+ | `texto` | 16.180 |
70
+ | `figurinha` | 4.806 |
71
+ | `reacao` | 1.502 |
72
+ | `imagem` | 1.298 |
73
+ | `outros` | 769 |
74
+ | `video` | 227 |
75
+ | `audio` | 213 |
76
76
  | `documento` | 5 |
77
77
 
78
78
  <details><summary>Comandos disponíveis (62)</summary>
@@ -8,6 +8,8 @@ const DEFAULT_AUTO_PACK_NAME = process.env.STICKER_PACK_AUTO_PACK_NAME || 'pack'
8
8
  const AUTO_PACK_TARGET_VISIBILITY = 'unlisted';
9
9
  const AUTO_COLLECT_ENABLED = process.env.STICKER_PACK_AUTO_COLLECT_ENABLED !== 'false';
10
10
  const AUTO_PACK_NAME_MAX_LENGTH = 120;
11
+ const AUTO_PACK_DESCRIPTION_MARKER = '[auto-pack:collector]';
12
+ const AUTO_PACK_DESCRIPTION_TEXT = 'Coleção automática de figurinhas criadas pelo usuário.';
11
13
  const normalizeVisibility = (value) => String(value || '').trim().toLowerCase();
12
14
 
13
15
  /**
@@ -43,6 +45,42 @@ const normalizeAutoPackName = (value, { fallback = 'pack', maxLength = AUTO_PACK
43
45
  return normalized || fallback;
44
46
  };
45
47
 
48
+ const buildAutoPackDescription = () => `${AUTO_PACK_DESCRIPTION_TEXT} ${AUTO_PACK_DESCRIPTION_MARKER}`.trim();
49
+
50
+ const isThemeCurationAutoPack = (pack) => {
51
+ if (!pack || typeof pack !== 'object') return false;
52
+ const description = String(pack.description || '').toLowerCase();
53
+ if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
54
+ if (String(pack.name || '').trim().toLowerCase().startsWith('[auto]')) return true;
55
+ return Boolean(String(pack.pack_theme_key || '').trim());
56
+ };
57
+
58
+ const isAutoCollectorPack = (pack) => {
59
+ if (!pack || typeof pack !== 'object') return false;
60
+ if (isThemeCurationAutoPack(pack)) return false;
61
+
62
+ const description = String(pack.description || '').toLowerCase();
63
+ if (description.includes(AUTO_PACK_DESCRIPTION_MARKER)) return true;
64
+ if (description.includes('coleção automática de figurinhas criadas pelo usuário.')) return true;
65
+
66
+ const normalizedName = normalizeAutoPackName(pack.name, { fallback: '', maxLength: AUTO_PACK_NAME_MAX_LENGTH });
67
+ if (!normalizedName) return false;
68
+
69
+ const base = normalizeAutoPackName(DEFAULT_AUTO_PACK_NAME, { fallback: 'pack', maxLength: AUTO_PACK_NAME_MAX_LENGTH });
70
+ const matcher = new RegExp(`^${escapeRegex(base.toLowerCase())}\\d+$`, 'i');
71
+ const looksLikeLegacyCollector = matcher.test(normalizedName);
72
+ if (looksLikeLegacyCollector) return true;
73
+
74
+ return pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1;
75
+ };
76
+
77
+ const isUserManagedPackCandidate = (pack) => {
78
+ if (!pack || typeof pack !== 'object') return false;
79
+ if (isThemeCurationAutoPack(pack)) return false;
80
+ if (isAutoCollectorPack(pack)) return false;
81
+ return Number(pack.is_auto_pack || 0) !== 1;
82
+ };
83
+
46
84
  /**
47
85
  * Monta candidato incremental para nome de pack automático.
48
86
  *
@@ -177,21 +215,37 @@ export function createAutoPackCollector(options = {}) {
177
215
  const ensureTargetPack = async ({ ownerJid, senderName }) => {
178
216
  const packs = await deps.stickerPackService.listPacks({ ownerJid, limit: 30 });
179
217
  if (packs.length > 0) {
218
+ // Prioriza pack gerenciado pelo usuário (pack atual/manual).
219
+ const userManagedPacks = packs.filter((entry) => isUserManagedPackCandidate(entry));
220
+ if (userManagedPacks.length > 0) {
221
+ return {
222
+ pack: userManagedPacks[0],
223
+ packs,
224
+ };
225
+ }
226
+
227
+ const managedAutoPacks = packs.filter((entry) => isAutoCollectorPack(entry));
180
228
  const preferredPack =
181
- packs.find((entry) => normalizeVisibility(entry?.visibility) === AUTO_PACK_TARGET_VISIBILITY) || packs[0];
182
- const ensuredPack = await ensureAutoPackVisibility({ ownerJid, pack: preferredPack });
183
- return {
184
- pack: ensuredPack,
185
- packs,
186
- };
229
+ managedAutoPacks.find((entry) => normalizeVisibility(entry?.visibility) === AUTO_PACK_TARGET_VISIBILITY)
230
+ || managedAutoPacks[0]
231
+ || null;
232
+
233
+ if (preferredPack) {
234
+ const ensuredPack = await ensureAutoPackVisibility({ ownerJid, pack: preferredPack });
235
+ return {
236
+ pack: ensuredPack,
237
+ packs,
238
+ };
239
+ }
187
240
  }
188
241
 
189
242
  const created = await deps.stickerPackService.createPack({
190
243
  ownerJid,
191
244
  name: makeAutoPackName([]),
192
245
  publisher: sanitizeText(senderName, 120, { allowEmpty: true }) || 'OmniZap',
193
- description: 'Coleção automática de figurinhas criadas pelo usuário.',
246
+ description: buildAutoPackDescription(),
194
247
  visibility: AUTO_PACK_TARGET_VISIBILITY,
248
+ isAutoPack: true,
195
249
  });
196
250
 
197
251
  return {
@@ -250,8 +304,9 @@ export function createAutoPackCollector(options = {}) {
250
304
  ownerJid,
251
305
  name: makeAutoPackName(packs),
252
306
  publisher: sanitizeText(senderName, 120, { allowEmpty: true }) || targetPack.publisher || 'OmniZap',
253
- description: 'Coleção automática de figurinhas criadas pelo usuário.',
307
+ description: buildAutoPackDescription(),
254
308
  visibility: AUTO_PACK_TARGET_VISIBILITY,
309
+ isAutoPack: true,
255
310
  });
256
311
 
257
312
  const updated = await deps.stickerPackService.addStickerToPack({
@@ -1,10 +1,6 @@
1
1
  import 'dotenv/config';
2
2
 
3
- import http from 'node:http';
4
- import { randomUUID } from 'node:crypto';
5
- import { URL } from 'node:url';
6
3
  import client from 'prom-client';
7
- import logger from '../utils/logger/loggerModule.js';
8
4
 
9
5
  const parseEnvBool = (value, fallback) => {
10
6
  if (value === undefined || value === null || value === '') return fallback;
@@ -39,23 +35,12 @@ const QUERY_THRESHOLDS_MS = parseThresholds(process.env.DB_QUERY_ALERT_THRESHOLD
39
35
 
40
36
  const registry = new client.Registry();
41
37
  let metrics = null;
42
- let server = null;
43
- let serverStarted = false;
44
- let stickerCatalogModulePromise = null;
45
38
 
46
39
  const normalizeLabel = (value, fallback = 'unknown') => {
47
40
  if (value === undefined || value === null || value === '') return fallback;
48
41
  return String(value);
49
42
  };
50
43
 
51
- const normalizeRequestId = (value) => {
52
- const token = String(value || '')
53
- .trim()
54
- .replace(/[^a-zA-Z0-9._:-]/g, '')
55
- .slice(0, 120);
56
- return token || randomUUID();
57
- };
58
-
59
44
  const normalizeHttpMethod = (method) => {
60
45
  const normalized = String(method || '').trim().toUpperCase();
61
46
  if (!normalized) return 'UNKNOWN';
@@ -70,7 +55,7 @@ const toStatusClass = (statusCode) => {
70
55
  return `${head}xx`;
71
56
  };
72
57
 
73
- const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null } = {}) => {
58
+ export const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null } = {}) => {
74
59
  if (pathname?.startsWith(metricsPath)) return 'metrics';
75
60
  if (pathname === '/sitemap.xml') return 'sitemap';
76
61
  if (pathname === '/api/marketplace/stats') return 'marketplace_stats';
@@ -390,137 +375,23 @@ const ensureMetrics = () => {
390
375
  return metrics;
391
376
  };
392
377
 
393
- const loadStickerCatalogModule = async () => {
394
- if (!stickerCatalogModulePromise) {
395
- stickerCatalogModulePromise = import('../modules/stickerPackModule/stickerPackCatalogHttp.js');
396
- }
397
- return stickerCatalogModulePromise;
398
- };
399
-
400
378
  export const isMetricsEnabled = () => METRICS_ENABLED;
401
379
 
402
- export const startMetricsServer = () => {
403
- if (!METRICS_ENABLED || serverStarted) return;
404
- ensureMetrics();
405
- server = http.createServer(async (req, res) => {
406
- const requestStartedAt = Date.now();
407
- const requestId = normalizeRequestId(req.headers['x-request-id']);
408
- res.setHeader('X-Request-Id', requestId);
409
-
410
- const host = req.headers.host || `${METRICS_HOST}:${METRICS_PORT}`;
411
- let parsedUrl;
412
- try {
413
- parsedUrl = new URL(req.url || '/', `http://${host}`);
414
- } catch {
415
- parsedUrl = new URL(req.url || '/', 'http://localhost');
416
- }
417
- const pathname = parsedUrl.pathname;
418
- let routeGroup = resolveRouteGroup({
419
- pathname,
420
- metricsPath: METRICS_PATH,
421
- catalogConfig: null,
422
- });
423
-
424
- res.once('finish', () => {
425
- recordHttpRequest({
426
- durationMs: Date.now() - requestStartedAt,
427
- method: req.method,
428
- statusCode: res.statusCode,
429
- routeGroup,
430
- });
431
- });
432
-
433
- try {
434
- const stickerCatalogModule = await loadStickerCatalogModule();
435
- const catalogConfig =
436
- typeof stickerCatalogModule.getStickerCatalogConfig === 'function'
437
- ? stickerCatalogModule.getStickerCatalogConfig()
438
- : null;
439
- routeGroup = resolveRouteGroup({
440
- pathname,
441
- metricsPath: METRICS_PATH,
442
- catalogConfig,
443
- });
444
- const handledByCatalog = await stickerCatalogModule.maybeHandleStickerCatalogRequest(req, res, {
445
- pathname,
446
- url: parsedUrl,
447
- });
380
+ export const getMetricsServerConfig = () => ({
381
+ enabled: METRICS_ENABLED,
382
+ host: METRICS_HOST,
383
+ port: METRICS_PORT,
384
+ path: METRICS_PATH,
385
+ });
448
386
 
449
- if (handledByCatalog) {
450
- return;
451
- }
452
- } catch (error) {
453
- logger.error('Erro ao inicializar rotas web de sticker packs.', {
454
- error: error.message,
455
- });
456
- }
457
-
458
- if (!pathname.startsWith(METRICS_PATH)) {
459
- res.statusCode = 404;
460
- res.end('Not Found');
461
- return;
462
- }
463
-
464
- try {
465
- const body = await registry.metrics();
466
- res.statusCode = 200;
467
- res.setHeader('Content-Type', registry.contentType);
468
- res.end(body);
469
- } catch (error) {
470
- res.statusCode = 500;
471
- res.end('Metrics error');
472
- logger.error('Erro ao gerar /metrics', { error: error.message });
473
- }
474
- });
475
-
476
- server.listen(METRICS_PORT, METRICS_HOST, () => {
477
- serverStarted = true;
478
- logger.info('Servidor /metrics iniciado', {
479
- host: METRICS_HOST,
480
- port: METRICS_PORT,
481
- path: METRICS_PATH,
482
- });
483
-
484
- loadStickerCatalogModule()
485
- .then((module) => {
486
- const config = typeof module.getStickerCatalogConfig === 'function' ? module.getStickerCatalogConfig() : null;
487
- if (!config?.enabled) return;
488
- logger.info('Catalogo web de sticker packs habilitado', {
489
- web_path: config.webPath,
490
- api_base_path: config.apiBasePath,
491
- orphan_api_path: config.orphanApiPath,
492
- data_public_path: config.dataPublicPath,
493
- data_public_dir: config.dataPublicDir,
494
- host: METRICS_HOST,
495
- port: METRICS_PORT,
496
- });
497
- })
498
- .catch((error) => {
499
- logger.warn('Nao foi possivel carregar configuracao do catalogo de sticker packs.', {
500
- error: error.message,
501
- });
502
- });
503
- });
504
-
505
- server.on('error', (error) => {
506
- logger.error('Falha ao iniciar servidor /metrics', { error: error.message });
507
- });
508
- };
509
-
510
- export const stopMetricsServer = async () => {
511
- if (!serverStarted || !server) return;
512
- const current = server;
513
- server = null;
514
- serverStarted = false;
515
- await new Promise((resolve, reject) => {
516
- current.close((error) => {
517
- if (error) {
518
- reject(error);
519
- return;
520
- }
521
- resolve();
522
- });
523
- });
387
+ export const getMetricsPayload = async () => {
388
+ if (!METRICS_ENABLED) return null;
389
+ ensureMetrics();
390
+ const body = await registry.metrics();
391
+ return {
392
+ body,
393
+ contentType: registry.contentType,
394
+ };
524
395
  };
525
396
 
526
397
  export const recordError = (scope = 'app') => {
package/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Responsabilidades principais:
5
5
  * - Inicializar o banco (garantindo DB e tabelas).
6
- * - Subir servidor de métricas.
6
+ * - Subir servidor HTTP (catálogo web + endpoint de métricas).
7
7
  * - Rodar backfill do lid_map (opcional, em background).
8
8
  * - Conectar ao WhatsApp (Baileys).
9
9
  * - Iniciar serviços auxiliares (ex: broadcast de notícias).
@@ -25,7 +25,7 @@ import {
25
25
  stopNewsBroadcastService,
26
26
  } from './app/services/newsBroadcastService.js';
27
27
  import initializeDatabase from './database/init.js';
28
- import { startMetricsServer, stopMetricsServer } from './app/observability/metrics.js';
28
+ import { startHttpServer, stopHttpServer } from './server/index.js';
29
29
  import {
30
30
  startStickerClassificationBackground,
31
31
  stopStickerClassificationBackground,
@@ -198,7 +198,7 @@ async function closeDatabasePool() {
198
198
  *
199
199
  * Fluxo de startup:
200
200
  * 1) Inicializa DB (cria/verifica DB/tabelas)
201
- * 2) Sobe servidor de métricas
201
+ * 2) Sobe servidor HTTP (catálogo + métricas)
202
202
  * 3) Inicia backfill (opcional) em background
203
203
  * 4) Conecta no WhatsApp
204
204
  * 5) Inicia serviços auxiliares (news broadcast)
@@ -216,8 +216,8 @@ async function startApp() {
216
216
  await withTimeout(initializeDatabase(), DB_INIT_TIMEOUT_MS, 'Inicializacao do banco');
217
217
  logger.info('Banco de dados pronto.');
218
218
 
219
- logger.info('Inicializando servidor de metricas...');
220
- startMetricsServer();
219
+ logger.info('Inicializando servidor HTTP...');
220
+ startHttpServer();
221
221
  if (isStickerWorkerPipelineEnabled()) {
222
222
  startStickerWorkerPipeline();
223
223
  } else {
@@ -274,7 +274,7 @@ startApp();
274
274
  * - serviço de notícias (se stop existir)
275
275
  * - aguarda backfill (com timeout curto)
276
276
  * - encerra socket do WhatsApp
277
- * - encerra servidor de métricas
277
+ * - encerra servidor HTTP
278
278
  * - encerra pool do MySQL
279
279
  *
280
280
  * Regras:
@@ -339,14 +339,14 @@ async function shutdown(signal, error) {
339
339
  }
340
340
  }
341
341
 
342
- // 4) Encerrar servidor de métricas
343
- if (typeof stopMetricsServer === 'function') {
342
+ // 4) Encerrar servidor HTTP
343
+ if (typeof stopHttpServer === 'function') {
344
344
  try {
345
- logger.info('Encerrando servidor de metricas...');
346
- await withTimeout(stopMetricsServer(), 8000, 'Encerramento metricas');
347
- logger.info('Servidor de metricas encerrado.');
348
- } catch (metricsError) {
349
- logger.warn('Falha ao encerrar servidor de metricas.', { error: metricsError.message });
345
+ logger.info('Encerrando servidor HTTP...');
346
+ await withTimeout(stopHttpServer(), 8000, 'Encerramento HTTP');
347
+ logger.info('Servidor HTTP encerrado.');
348
+ } catch (httpError) {
349
+ logger.warn('Falha ao encerrar servidor HTTP.', { error: httpError.message });
350
350
  }
351
351
  }
352
352
 
@@ -0,0 +1,83 @@
1
+ # Sticker Catalog 10x Baseline e SLOs
2
+
3
+ Este documento define a baseline operacional da camada HTTP + pipeline de classificação para o módulo de stickers.
4
+
5
+ ## 1. Metas SLO (fase inicial)
6
+
7
+ ### HTTP catálogo (`/api/sticker-packs*`, `/stickers*`, `/api/marketplace/stats`)
8
+
9
+ - **Latência p95**: `<= 750ms`
10
+ - **Latência p99**: `<= 1500ms`
11
+ - **Taxa de erro (5xx + timeout)**: `<= 2%` por janela de 5 minutos
12
+ - **Throughput alvo**: escalar linearmente com workers/processos sem aumento abrupto do p95
13
+
14
+ ### Classificação de stickers
15
+
16
+ - **Duração média do ciclo**: `<= 10s`
17
+ - **Throughput mínimo (assets classificados/min)**: `>= 300` (ajustar por hardware)
18
+ - **Backlog de fila (`sticker_reprocess_pending`)**: tendência de queda após picos; alerta se cresce por mais de 15 min
19
+
20
+ ## 2. Métricas instrumentadas
21
+
22
+ ### HTTP
23
+
24
+ - `omnizap_http_requests_total{route_group,method,status_class}`
25
+ - `omnizap_http_request_duration_ms{route_group,method,status_class}`
26
+ - `omnizap_http_slo_violation_total{route_group,method}`
27
+
28
+ `route_group` segmenta tráfego em:
29
+
30
+ - `catalog_api_public`
31
+ - `catalog_api_auth`
32
+ - `catalog_api_admin`
33
+ - `catalog_api_upload`
34
+ - `catalog_web`
35
+ - `catalog_data_asset`
36
+ - `catalog_user_profile`
37
+ - `marketplace_stats`
38
+ - `metrics`
39
+ - `other`
40
+
41
+ ### Classificação
42
+
43
+ - `omnizap_sticker_classification_cycle_duration_ms{status}`
44
+ - `omnizap_sticker_classification_cycle_total{status}`
45
+ - `omnizap_sticker_classification_assets_total{outcome}`
46
+ - `omnizap_queue_depth{queue}`
47
+
48
+ ## 3. Tracing mínimo
49
+
50
+ - Cada request HTTP agora recebe/propaga `X-Request-Id`.
51
+ - Se o cliente enviar `X-Request-Id`, o valor é reaproveitado.
52
+ - Sem header, o servidor gera UUID.
53
+
54
+ ## 4. Baseline de carga (script local)
55
+
56
+ Script: `scripts/sticker-catalog-loadtest.mjs`
57
+
58
+ Exemplo:
59
+
60
+ ```bash
61
+ node scripts/sticker-catalog-loadtest.mjs \
62
+ --base-url http://127.0.0.1:9102 \
63
+ --duration-seconds 60 \
64
+ --concurrency 40 \
65
+ --paths "/api/sticker-packs?limit=24&sort=popular,/api/sticker-packs/stats,/api/sticker-packs/creators?limit=25" \
66
+ --out /tmp/sticker-loadtest-report.json
67
+ ```
68
+
69
+ Interpretação rápida:
70
+
71
+ - `latency_ms.p95 <= 750` = SLO de latência cumprido
72
+ - `error_rate <= 0.02` = estabilidade aceitável
73
+ - `throughput_rps` = referência para comparar antes/depois de otimizações
74
+
75
+ ## 5. Gate de rollout sugerido
76
+
77
+ 1. Capturar baseline com carga atual.
78
+ 2. Aplicar mudança de arquitetura/índice/cache.
79
+ 3. Reexecutar carga com mesmos parâmetros.
80
+ 4. Aprovar rollout apenas se:
81
+ - p95 não piorar mais de 10%
82
+ - erro não subir acima de 2%
83
+ - backlog voltar ao patamar normal em até 15 min
@@ -0,0 +1,128 @@
1
+ # Sticker 10x Hardening And Rollout
2
+
3
+ ## Scope
4
+
5
+ This runbook covers phases 4-8 of the sticker-pack scale plan:
6
+
7
+ 1. ranking snapshot read path
8
+ 2. internal outbox/event consumer
9
+ 3. dedicated workers (classification/curation/rebuild)
10
+ 4. object storage delivery with secure URLs
11
+ 5. canary rollout, rollback, and final tuning
12
+
13
+ ## Feature Flags
14
+
15
+ Flags are stored in `feature_flag`:
16
+
17
+ - `enable_ranking_snapshot_read`
18
+ - `enable_domain_event_outbox`
19
+ - `enable_worker_dedicated_processes`
20
+ - `enable_object_storage_delivery`
21
+
22
+ ### Query Current Status
23
+
24
+ ```sql
25
+ SELECT flag_name, is_enabled, rollout_percent, updated_at
26
+ FROM feature_flag
27
+ WHERE flag_name IN (
28
+ 'enable_ranking_snapshot_read',
29
+ 'enable_domain_event_outbox',
30
+ 'enable_worker_dedicated_processes',
31
+ 'enable_object_storage_delivery'
32
+ )
33
+ ORDER BY flag_name;
34
+ ```
35
+
36
+ ### Update Rollout Percent
37
+
38
+ ```sql
39
+ UPDATE feature_flag
40
+ SET is_enabled = 1, rollout_percent = 25, updated_by = 'ops'
41
+ WHERE flag_name = 'enable_worker_dedicated_processes';
42
+ ```
43
+
44
+ ### Emergency Disable
45
+
46
+ ```sql
47
+ UPDATE feature_flag
48
+ SET is_enabled = 0, rollout_percent = 0, updated_by = 'ops'
49
+ WHERE flag_name IN (
50
+ 'enable_worker_dedicated_processes',
51
+ 'enable_object_storage_delivery',
52
+ 'enable_domain_event_outbox'
53
+ );
54
+ ```
55
+
56
+ ## Canary Sequence
57
+
58
+ 1. `enable_ranking_snapshot_read`: 10% -> 50% -> 100%
59
+ 2. `enable_domain_event_outbox`: 10% -> 50% -> 100%
60
+ 3. start dedicated worker processes and set `enable_worker_dedicated_processes`: 10% -> 50% -> 100%
61
+ 4. `enable_object_storage_delivery`: 5% -> 25% -> 100%
62
+
63
+ Promotion gate for each step:
64
+
65
+ - HTTP p95 within target
66
+ - queue backlog stable (`pending`, `failed`)
67
+ - outbox DLQ not growing unexpectedly
68
+ - no sustained error-rate increase
69
+
70
+ ## Dedicated Workers
71
+
72
+ Run workers as isolated processes:
73
+
74
+ ```bash
75
+ npm run worker:sticker:classification
76
+ npm run worker:sticker:curation
77
+ npm run worker:sticker:rebuild
78
+ ```
79
+
80
+ PM2 production profile includes these workers in `ecosystem.prod.config.cjs`.
81
+
82
+ ## 10x Validation
83
+
84
+ ### HTTP Stress
85
+
86
+ ```bash
87
+ npm run loadtest:stickers -- --base-url http://127.0.0.1:9102 --duration-seconds 120 --concurrency 200 --slo-ms 750
88
+ ```
89
+
90
+ ### Queue/Worker Validation
91
+
92
+ Monitor:
93
+
94
+ - `sticker_worker_tasks_pending`
95
+ - `sticker_worker_tasks_processing`
96
+ - `sticker_worker_tasks_failed`
97
+ - `domain_event_outbox_pending`
98
+ - `domain_event_outbox_failed`
99
+
100
+ Acceptance:
101
+
102
+ - failed queues remain near zero (transient spikes allowed)
103
+ - pending queues recover after load burst
104
+ - no monotonic growth in DLQ tables
105
+
106
+ ## Rollback Plan
107
+
108
+ 1. Disable `enable_object_storage_delivery`.
109
+ 2. Disable `enable_worker_dedicated_processes` (inline poller resumes).
110
+ 3. Disable `enable_domain_event_outbox` if event flow is unstable.
111
+ 4. Keep `enable_ranking_snapshot_read` enabled only if snapshot freshness is healthy.
112
+
113
+ Data safety notes:
114
+
115
+ - tasks/events are persisted in SQL queues
116
+ - failed terminal tasks/events are preserved in DLQ tables
117
+ - local disk read path remains fallback for sticker asset serving
118
+
119
+ ## Post-Rollout Tuning
120
+
121
+ Tune these env vars after baseline:
122
+
123
+ - `STICKER_WORKER_CLASSIFICATION_CADENCE_MS`
124
+ - `STICKER_WORKER_CURATION_CADENCE_MS`
125
+ - `STICKER_WORKER_REBUILD_CADENCE_MS`
126
+ - `STICKER_DEDICATED_WORKER_POLL_INTERVAL_MS`
127
+ - `STICKER_SCORE_SNAPSHOT_REFRESH_INTERVAL_MS`
128
+ - `STICKER_OBJECT_STORAGE_SIGNED_URL_TTL_SECONDS`