@kaikybrofc/omnizap-system 2.2.5 → 2.2.6
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.
- package/.prettierrc +16 -0
- package/README.md +10 -10
- package/app/modules/stickerPackModule/autoPackCollectorService.js +63 -8
- package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +33 -7
- package/kaikybrofc-omnizap-system-2.2.6.tgz +0 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +3 -35
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.avif +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/assets/images/hero-banner-1280.webp +0 -0
- package/public/assets/images/hero-banner-720.avif +0 -0
- package/public/assets/images/hero-banner-720.webp +0 -0
- package/public/index.html +91 -16
- package/public/js/apps/homeApp.js +469 -353
- package/public/robots.txt +9 -0
- package/public/sitemap.xml +28 -0
- package/observability/mysql-exporter.cnf +0 -5
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,25 +53,25 @@ 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-
|
|
56
|
+
> Atualizado em `2026-02-28T06:26:02.459Z` | 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 |
|
|
63
|
-
| Stickers | 6.
|
|
64
|
-
| Mensagens registradas | 440.
|
|
62
|
+
| Packs | 293 |
|
|
63
|
+
| Stickers | 6.775 |
|
|
64
|
+
| Mensagens registradas | 440.765 |
|
|
65
65
|
|
|
66
66
|
#### Tipos de mensagem mais usados (amostra: 25.000)
|
|
67
67
|
| Tipo | Total |
|
|
68
68
|
| --- | ---: |
|
|
69
|
-
| `texto` | 16.
|
|
70
|
-
| `figurinha` | 4.
|
|
71
|
-
| `reacao` | 1.
|
|
72
|
-
| `imagem` | 1.
|
|
73
|
-
| `outros` |
|
|
74
|
-
| `video` |
|
|
69
|
+
| `texto` | 16.279 |
|
|
70
|
+
| `figurinha` | 4.721 |
|
|
71
|
+
| `reacao` | 1.504 |
|
|
72
|
+
| `imagem` | 1.290 |
|
|
73
|
+
| `outros` | 757 |
|
|
74
|
+
| `video` | 228 |
|
|
75
75
|
| `audio` | 216 |
|
|
76
76
|
| `documento` | 5 |
|
|
77
77
|
|
|
@@ -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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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:
|
|
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:
|
|
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({
|
|
@@ -157,6 +157,7 @@ const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_P
|
|
|
157
157
|
const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PATH, '/user');
|
|
158
158
|
const STICKER_DATA_PUBLIC_PATH = normalizeBasePath(process.env.STICKER_DATA_PUBLIC_PATH, '/data');
|
|
159
159
|
const STICKER_DATA_PUBLIC_DIR = path.resolve(process.env.STICKER_DATA_PUBLIC_DIR || path.join(process.cwd(), 'data'));
|
|
160
|
+
const STICKER_WEB_ASSET_VERSION = sanitizeText(process.env.STICKER_WEB_ASSET_VERSION || '', 64, { allowEmpty: true }) || '';
|
|
160
161
|
const CATALOG_PUBLIC_DIR = path.resolve(process.cwd(), 'public');
|
|
161
162
|
const CATALOG_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'index.html');
|
|
162
163
|
const CREATE_PACK_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'create', 'index.html');
|
|
@@ -171,7 +172,19 @@ const MAX_ORPHAN_LIST_LIMIT = clampInt(process.env.STICKER_ORPHAN_LIST_MAX_LIMIT
|
|
|
171
172
|
const DEFAULT_DATA_LIST_LIMIT = clampInt(process.env.STICKER_DATA_LIST_LIMIT, 50, 1, 200);
|
|
172
173
|
const MAX_DATA_LIST_LIMIT = clampInt(process.env.STICKER_DATA_LIST_MAX_LIMIT, 200, 1, 500);
|
|
173
174
|
const MAX_DATA_SCAN_FILES = clampInt(process.env.STICKER_DATA_SCAN_MAX_FILES, 10000, 100, 50000);
|
|
174
|
-
const ASSET_CACHE_SECONDS = clampInt(
|
|
175
|
+
const ASSET_CACHE_SECONDS = clampInt(
|
|
176
|
+
process.env.STICKER_WEB_ASSET_CACHE_SECONDS,
|
|
177
|
+
60 * 60 * 24 * 30,
|
|
178
|
+
60 * 60,
|
|
179
|
+
60 * 60 * 24 * 365,
|
|
180
|
+
);
|
|
181
|
+
const STATIC_TEXT_CACHE_SECONDS = clampInt(process.env.STICKER_WEB_STATIC_TEXT_CACHE_SECONDS, 60 * 60, 60, 60 * 60 * 24 * 30);
|
|
182
|
+
const IMMUTABLE_ASSET_CACHE_SECONDS = clampInt(
|
|
183
|
+
process.env.STICKER_WEB_IMMUTABLE_ASSET_CACHE_SECONDS,
|
|
184
|
+
60 * 60 * 24 * 365,
|
|
185
|
+
60 * 60,
|
|
186
|
+
60 * 60 * 24 * 365,
|
|
187
|
+
);
|
|
175
188
|
const STICKER_WEB_WHATSAPP_MESSAGE_TEMPLATE =
|
|
176
189
|
String(process.env.STICKER_WEB_WHATSAPP_MESSAGE_TEMPLATE || '/pack send {{pack_key}}').trim() ||
|
|
177
190
|
'/pack send {{pack_key}}';
|
|
@@ -1972,10 +1985,15 @@ const clearGoogleWebSessionCookie = (req, res) => {
|
|
|
1972
1985
|
};
|
|
1973
1986
|
|
|
1974
1987
|
const sendAsset = (req, res, buffer, mimetype = 'image/webp') => {
|
|
1988
|
+
const maxAgeSeconds = Math.max(60 * 60 * 24, ASSET_CACHE_SECONDS);
|
|
1989
|
+
const staleWhileRevalidateSeconds = Math.min(60 * 60 * 24 * 7, Math.max(300, maxAgeSeconds));
|
|
1975
1990
|
res.statusCode = 200;
|
|
1976
1991
|
res.setHeader('Content-Type', mimetype);
|
|
1977
1992
|
res.setHeader('Content-Length', String(buffer.length));
|
|
1978
|
-
res.setHeader(
|
|
1993
|
+
res.setHeader(
|
|
1994
|
+
'Cache-Control',
|
|
1995
|
+
`public, max-age=${maxAgeSeconds}, stale-while-revalidate=${staleWhileRevalidateSeconds}`,
|
|
1996
|
+
);
|
|
1979
1997
|
if (req.method === 'HEAD') {
|
|
1980
1998
|
res.end();
|
|
1981
1999
|
return;
|
|
@@ -2214,8 +2232,12 @@ const buildStickerAssetUrl = (packKey, stickerId) =>
|
|
|
2214
2232
|
`${STICKER_API_BASE_PATH}/${encodeURIComponent(packKey)}/stickers/${encodeURIComponent(stickerId)}.webp`;
|
|
2215
2233
|
const buildOrphanStickersApiUrl = () => STICKER_ORPHAN_API_PATH;
|
|
2216
2234
|
const buildDataAssetApiBaseUrl = () => `${STICKER_API_BASE_PATH}/data-files`;
|
|
2217
|
-
const
|
|
2218
|
-
const
|
|
2235
|
+
const CATALOG_STYLES_WEB_PATH = `${STICKER_WEB_PATH}/assets/styles.css`;
|
|
2236
|
+
const CATALOG_SCRIPT_WEB_PATH = `${STICKER_WEB_PATH}/assets/catalog.js`;
|
|
2237
|
+
const appendAssetVersionQuery = (assetPath) =>
|
|
2238
|
+
STICKER_WEB_ASSET_VERSION ? `${assetPath}?v=${encodeURIComponent(STICKER_WEB_ASSET_VERSION)}` : assetPath;
|
|
2239
|
+
const buildCatalogStylesUrl = () => appendAssetVersionQuery(CATALOG_STYLES_WEB_PATH);
|
|
2240
|
+
const buildCatalogScriptUrl = () => appendAssetVersionQuery(CATALOG_SCRIPT_WEB_PATH);
|
|
2219
2241
|
const buildDataAssetUrl = (relativePath) =>
|
|
2220
2242
|
`${STICKER_DATA_PUBLIC_PATH}/${String(relativePath)
|
|
2221
2243
|
.split('/')
|
|
@@ -3467,9 +3489,13 @@ const handleSitemapRequest = async (req, res) => {
|
|
|
3467
3489
|
const sendStaticTextFile = async (req, res, filePath, contentType) => {
|
|
3468
3490
|
try {
|
|
3469
3491
|
const body = await fs.readFile(filePath, 'utf8');
|
|
3492
|
+
const hasVersionQuery = /(?:\?|&)v=/.test(String(req.url || ''));
|
|
3493
|
+
const cacheControl = hasVersionQuery
|
|
3494
|
+
? `public, max-age=${IMMUTABLE_ASSET_CACHE_SECONDS}, immutable`
|
|
3495
|
+
: `public, max-age=${STATIC_TEXT_CACHE_SECONDS}, stale-while-revalidate=${Math.min(86400, STATIC_TEXT_CACHE_SECONDS * 4)}`;
|
|
3470
3496
|
res.statusCode = 200;
|
|
3471
3497
|
res.setHeader('Content-Type', contentType);
|
|
3472
|
-
res.setHeader('Cache-Control',
|
|
3498
|
+
res.setHeader('Cache-Control', cacheControl);
|
|
3473
3499
|
if (req.method === 'HEAD') {
|
|
3474
3500
|
res.end();
|
|
3475
3501
|
return true;
|
|
@@ -3493,11 +3519,11 @@ const sendStaticTextFile = async (req, res, filePath, contentType) => {
|
|
|
3493
3519
|
};
|
|
3494
3520
|
|
|
3495
3521
|
const handleCatalogStaticAssetRequest = async (req, res, pathname) => {
|
|
3496
|
-
if (pathname ===
|
|
3522
|
+
if (pathname === CATALOG_STYLES_WEB_PATH) {
|
|
3497
3523
|
return sendStaticTextFile(req, res, CATALOG_STYLES_FILE_PATH, 'text/css; charset=utf-8');
|
|
3498
3524
|
}
|
|
3499
3525
|
|
|
3500
|
-
if (pathname ===
|
|
3526
|
+
if (pathname === CATALOG_SCRIPT_WEB_PATH) {
|
|
3501
3527
|
return sendStaticTextFile(req, res, CATALOG_SCRIPT_FILE_PATH, 'application/javascript; charset=utf-8');
|
|
3502
3528
|
}
|
|
3503
3529
|
|
|
Binary file
|
|
@@ -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`
|
package/package.json
CHANGED
|
@@ -1,45 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaikybrofc/omnizap-system",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"description": "Sistema profissional de automação WhatsApp com tecnologia Baileys",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"registry": "https://npm.pkg.github.com"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
10
|
-
"app/**/*.js",
|
|
11
|
-
"database/**/*.js",
|
|
12
|
-
"database/migrations/**/*.sql",
|
|
13
|
-
"public/**/*.html",
|
|
14
|
-
"public/**/*.css",
|
|
15
|
-
"public/**/*.js",
|
|
16
|
-
"scripts/**/*.sh",
|
|
17
|
-
"scripts/**/*.mjs",
|
|
18
|
-
"ml/clip_classifier/adaptive_scoring.py",
|
|
19
|
-
"ml/clip_classifier/classifier.py",
|
|
20
|
-
"ml/clip_classifier/embedding_store.py",
|
|
21
|
-
"ml/clip_classifier/env_loader.py",
|
|
22
|
-
"ml/clip_classifier/llm_label_expander.py",
|
|
23
|
-
"ml/clip_classifier/main.py",
|
|
24
|
-
"ml/clip_classifier/similarity_engine.py",
|
|
25
|
-
"ml/clip_classifier/requirements.txt",
|
|
26
|
-
"ml/clip_classifier/README.md",
|
|
27
|
-
"ml/clip_classifier/Dockerfile",
|
|
28
|
-
"observability/**/*.yml",
|
|
29
|
-
"observability/**/*.yaml",
|
|
30
|
-
"observability/**/*.json",
|
|
31
|
-
"observability/**/*.sql",
|
|
32
|
-
"observability/**/*.cnf",
|
|
33
|
-
"index.js",
|
|
34
|
-
"docker-compose.yml",
|
|
35
|
-
"ecosystem.prod.config.cjs",
|
|
36
|
-
"eslint.config.js",
|
|
37
|
-
".env.example",
|
|
38
|
-
"README.md",
|
|
39
|
-
"RELEASE-v2.1.2.md",
|
|
40
|
-
"LICENSE",
|
|
41
|
-
"package-lock.json"
|
|
42
|
-
],
|
|
43
9
|
"type": "module",
|
|
44
10
|
"keywords": [
|
|
45
11
|
"whatsapp",
|
|
@@ -52,6 +18,8 @@
|
|
|
52
18
|
"author": "OmniZap Team",
|
|
53
19
|
"license": "MIT",
|
|
54
20
|
"scripts": {
|
|
21
|
+
"prepack": "npm run sync:npmignore",
|
|
22
|
+
"sync:npmignore": "node -e \"const fs=require('node:fs');fs.copyFileSync('.gitignore','.npmignore');console.log('synced .npmignore from .gitignore');\"",
|
|
55
23
|
"start": "node index.js",
|
|
56
24
|
"dev": "node index.js",
|
|
57
25
|
"db:init": "node database/init.js",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|