funifier-mcp 0.2.26 → 0.2.27
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/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +3 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,189 +1,931 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `trigger`
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/trigger`
|
|
4
4
|
**API Endpoint:** `/v3/trigger`
|
|
5
|
+
**Coleção MongoDB:** `trigger` (definições) + `trigger_log` (logs de execução)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
---
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
## 1. Visão Geral
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
O módulo `trigger` é o motor de **execução de scripts Groovy** acionados por eventos do ciclo de vida das entidades da plataforma. Cada documento `trigger` armazena um script Groovy que será compilado, cacheado em memória e executado quando uma combinação `(entity, event)` for disparada por algum outro manager do serviço.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
- Para integrar com APIs externas (Zapier, CRM, etc.)
|
|
14
|
-
- Para alterar pontuação com regras especiais
|
|
15
|
-
- Para executar lógica de negócio personalizada
|
|
16
|
-
- **Importante:** usar apenas quando as configurações padrão não atendem
|
|
13
|
+
Papel arquitetural:
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
- Único ponto de extensibilidade server-side da plataforma — permite estender qualquer comportamento da gamificação sem recompilar o serviço.
|
|
16
|
+
- Compila o script com `GroovyClassLoader` configurado por `SecureASTCustomizer` + `TriggerExpressionChecker` (sandbox parcial — ver seção 8).
|
|
17
|
+
- Mantém cache de classes compiladas indexado por `trigger.id` (`TriggerManager.compiled`), invalidado por `trigger.updated`.
|
|
18
|
+
- Executa cada disparo em uma thread dedicada (`Executors.newSingleThreadExecutor()`), com timeout duro por trigger.
|
|
19
|
+
- É chamado de forma síncrona — `TriggerManager.execute(...)` é uma chamada bloqueante que aguarda todos os triggers daquela combinação `(entity, event)` rodarem antes de retornar ao manager chamador.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
- [ ] Definir entidade observada (player, challenge, action, achievement, etc.)
|
|
22
|
-
- [ ] Definir evento (before_create, after_create, before_win, after_win, etc.)
|
|
23
|
-
- [ ] Escrever script Java com método `void trigger(event, entity, player, database)`
|
|
24
|
-
- [ ] Testar trigger em ambiente de homologação
|
|
21
|
+
Quem dispara o módulo (encontrado no código — `getTriggerManager().execute(...)`):
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
- `ActionManager.trackSynchonous` — `(action, before_win)` e `(action, after_win)` em volta do `addLog`.
|
|
24
|
+
- `AchievementManager.fireAction` — `(point_category, before/after_win)`, `(challenge, before/after_win)`, `(<collection>, before/after_win)` em torno de cada Achievement persistido.
|
|
25
|
+
- `AchievementManager.evaluateLevelUp` — `(level, before/after_win)` no instante do level up.
|
|
26
|
+
- `CharacterStarManager.evaluateLevelUp` — `(character_star_stats_level, before/after_win)`.
|
|
27
|
+
- `DatabaseManager.bulkInsert` — `(<collection>, before_bulk)`, `(<collection>, before_create)`, `(<collection>, after_create)` por item, `(<collection>, after_bulk)` no final.
|
|
28
|
+
- `DatabaseRest.insert/update/delete` — `(<collection>, before_create|after_create|before_update|after_update|before_delete|after_delete)`.
|
|
29
|
+
- `CatalogManager.purchase` / `PurchaseManager.buy` — `(catalog_item, before_purchase_validation)`, `(catalog_item, before_win)`, `(catalog_item, after_win)`.
|
|
30
|
+
- `SwapManager.*` — `(swap, before/after_create|delete|win)`, `(swap, before_acquire_validation)`, `(swap_counter_offer, ...)`.
|
|
31
|
+
- `MysteryBoxManager.execute` — `(mystery_box, before_win)`, `(mystery_box, before_lose)`, `(mystery_box, before_win_reward)`, `(mystery_box, after_win|after_lose)`.
|
|
32
|
+
- `LotteryManager.execute` — `(lottery_ticket, before/after_win)`.
|
|
33
|
+
- `CompetitionManager.execute/insert/insertJoin` — `(competition, ...)`.
|
|
34
|
+
- `CrowningManager.calculateLeaderBoardV3/calculateCrown` — `(crown, ...)`.
|
|
35
|
+
- `QuizManager.insert/start/finish`, `QuestionManager.insert/insertLog` — eventos `before/after_create|update`.
|
|
36
|
+
- `FolderManager.insertLog/deleteLog` / `FolderRest.progress` — `(folder_log, before/after_create|delete)` e `("folder_progress", after_create)`.
|
|
37
|
+
- `PlayerManager.insert/delete`, `PlayerRest.changePassword(Request)` — `(player, before/after_create|delete)` e `(password_change, ...)`.
|
|
38
|
+
- `BackupManager.asyncExecute`, `CompactManager.asyncExecute`, `BetManager.delete`, `CsvManager.insertBulkSync(Helper)`, `UploadRest.uploadFile`, `CrmManager.*` — disparam eventos próprios sobre suas coleções.
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
|---------|--------|-----------|
|
|
30
|
-
| before_ | create | Antes de criar |
|
|
31
|
-
| after_ | create | Depois de criar |
|
|
32
|
-
| before_ | update | Antes de atualizar |
|
|
33
|
-
| after_ | delete | Depois de deletar |
|
|
34
|
-
| before_ | win | Antes de conquistar |
|
|
35
|
-
| after_ | win | Depois de conquistar |
|
|
40
|
+
Total: **29 arquivos** invocando `manager.getTriggerManager().execute(...)`, **146 sites de chamada** no `src/main/java` (sem testes).
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
Relação com outros módulos:
|
|
38
43
|
|
|
39
|
-
-
|
|
44
|
+
- **Statistic / GamificationLimits** — antes de cada execução, `SystemFactory.getInstance().getStatisticManager(apiKey).newTriggerExecution(trigger.id)` é chamado. Se o limite diário (`GamificationLimits.dailyTriggerExecutions`) for excedido, a execução é silenciosamente bloqueada (apenas um `System.out.println` de erro).
|
|
45
|
+
- **TriggerExpressionChecker** — bloqueia uso de `.execute`, `.getDB`, `.getMongo`, `.dropDatabase` e dos tipos `System`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject` no AST.
|
|
46
|
+
- **GameDao** — wrapper fino sobre Jongo que é injetado como `database` em todo script.
|
|
40
47
|
|
|
41
|
-
|
|
48
|
+
---
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
**Método:** GET
|
|
45
|
-
**Endpoint:** `/v3/trigger`
|
|
50
|
+
## 2. Arquitetura e Fluxos
|
|
46
51
|
|
|
47
|
-
###
|
|
48
|
-
**Método:** POST
|
|
49
|
-
**Endpoint:** `/v3/trigger`
|
|
52
|
+
### 2.1 Classes envolvidas
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
| Classe | Arquivo | Papel |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `Trigger` | `engine/integration/trigger/Trigger.java` | Entidade persistida — script + metadata. |
|
|
57
|
+
| `Reference` | `engine/integration/trigger/Reference.java` | Subdocumento (vínculo visual com outros itens da estratégia). |
|
|
58
|
+
| `ObjectStyle` | `engine/integration/trigger/ObjectStyle.java` | Subdocumento (`background`, `visible`) para apresentação no Studio. |
|
|
59
|
+
| `TriggerContext` | `engine/integration/trigger/TriggerContext.java` | Objeto de transporte runtime (`manager`, `origin`, `extra`). |
|
|
60
|
+
| `TriggerDaoMongo` | `engine/integration/trigger/TriggerDaoMongo.java` | DAO Jongo direto (CRUD + `findByEntityAndEvent`). |
|
|
61
|
+
| `TriggerManager` | `engine/integration/trigger/TriggerManager.java` | Orquestrador — mantém os caches `compiled` e `dates`. |
|
|
62
|
+
| `TriggerRunner` | `engine/integration/trigger/TriggerRunner.java` | Compila e roda o script em executor isolado com timeout. |
|
|
63
|
+
| `TriggerExpressionChecker` | `engine/integration/trigger/TriggerExpressionChecker.java` | `SecureASTCustomizer.ExpressionChecker` — bloqueia tokens/classes proibidas. |
|
|
64
|
+
| `TriggerLog` | `engine/integration/trigger/TriggerLog.java` | Documento gravado em `trigger_log` após cada execução. |
|
|
65
|
+
| `GameDao` | `engine/integration/trigger/GameDao.java` | Wrapper sobre Jongo passado ao script como `database`. |
|
|
66
|
+
| `TriggerStatistics` | `engine/stats/TriggerStatistics.java` | Contadores agregados de execução por trigger / dia / hora / mês. |
|
|
67
|
+
| `TriggerDetailStatistics` | `engine/stats/TriggerDetailStatistics.java` | Mesmas estatísticas por `trigger.id`. |
|
|
68
|
+
| `TriggerHtml` | `engine/integration/html/TriggerHtml.java` | **Não é trigger server-side.** Modelo client-side — coleção `trigger_html` — usado pelo SDK web. |
|
|
69
|
+
| `TriggerRest` | `rest/v3/rest/TriggerRest.java` | Endpoints REST `/v3/trigger`. |
|
|
70
|
+
|
|
71
|
+
### 2.2 Pipeline principal — `TriggerManager.execute(id, o, entity, event, player, context)`
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
[1] start = System.currentTimeMillis()
|
|
75
|
+
[2] runner = new TriggerRunner()
|
|
76
|
+
[3] Para cada trigger em mongo.findByEntityAndEvent(entity, event):
|
|
77
|
+
[3.1] limitOk = SystemFactory.getInstance()
|
|
78
|
+
.getStatisticManager(manager.getApiKey())
|
|
79
|
+
.newTriggerExecution(trigger.id)
|
|
80
|
+
[3.2] Se limitOk:
|
|
81
|
+
runner.run(trigger, o, player, manager, context)
|
|
82
|
+
Senão:
|
|
83
|
+
System.out.println("Error | ... You have exceeded your trigger daily executions limits")
|
|
84
|
+
(trigger NÃO executa, NÃO há log gerado, NÃO há exceção propagada)
|
|
85
|
+
[3.3] Se qualquer Exception subir do runner:
|
|
86
|
+
e.printStackTrace() — engole o erro, continua para próximo trigger
|
|
87
|
+
[4] Retorna (int) ms gastos no laço inteiro
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Observações críticas:
|
|
91
|
+
|
|
92
|
+
- O parâmetro `id` é o "id do item que originou o evento" (ex: `action.getActionId()`) — **NÃO é o id da trigger**. Ele não é usado para filtrar quais triggers rodam; serve apenas como dado disponível ao script.
|
|
93
|
+
- `findByEntityAndEvent` é uma query MongoDB direta — **não há cache de triggers ativos** (mas há cache de classes compiladas):
|
|
94
|
+
```js
|
|
95
|
+
db.trigger.find({ entity: <entity>, event: <event> })
|
|
96
|
+
```
|
|
97
|
+
- Se ninguém criou um trigger para `(entity, event)`, o laço é um no-op (`O(1)` do MongoDB) e retorna em milissegundos.
|
|
98
|
+
|
|
99
|
+
### 2.3 Diagrama — Pipeline de avaliação
|
|
100
|
+
|
|
101
|
+
```mermaid
|
|
102
|
+
flowchart LR
|
|
103
|
+
A[Caller<br/>ex: ActionManager.trackSynchonous] --> B[TriggerManager.execute<br/>id, o, entity, event, player, ctx]
|
|
104
|
+
B --> C[mongo.findByEntityAndEvent]
|
|
105
|
+
C --> D{Para cada<br/>trigger}
|
|
106
|
+
D --> E[StatisticManager<br/>newTriggerExecution]
|
|
107
|
+
E -->|limit OK| F[TriggerRunner.run]
|
|
108
|
+
E -->|excedido| X[System.out.println<br/>silencia]
|
|
109
|
+
F --> G[Cache de classe<br/>compiled trigger.id]
|
|
110
|
+
G -->|hit + dates > updated| H[executor.submit<br/>Callable]
|
|
111
|
+
G -->|miss / stale| I[getScript<br/>compile<br/>cache]
|
|
112
|
+
I --> H
|
|
113
|
+
H --> J{future.get<br/>timeout}
|
|
114
|
+
J -->|OK| K[addLog trigger_log]
|
|
115
|
+
J -->|TimeoutException| L[exceptions.add<br/>future.cancel<br/>executor.shutdownNow]
|
|
116
|
+
L --> K
|
|
117
|
+
D -->|throw qualquer| Y[e.printStackTrace<br/>continua loop]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2.4 Sub-pipeline — `TriggerRunner.run(...)`
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
[1] start = System.currentTimeMillis()
|
|
124
|
+
[2] exceptions = []; outputs = []
|
|
125
|
+
[3] Se trigger.updated == null:
|
|
126
|
+
manager.getTriggerManager().add(trigger)
|
|
127
|
+
(efeito colateral: persiste o trigger no mongo só para popular updated;
|
|
128
|
+
usado pelo modo cluster — qualquer nó que receber um trigger sem updated
|
|
129
|
+
força um save antes de seguir)
|
|
130
|
+
[4] lastCompilation = TriggerManager.dates.get(trigger.id)
|
|
131
|
+
[5] clazz = TriggerManager.compiled.get(trigger.id)
|
|
132
|
+
[6] Se clazz != null && lastCompilation > trigger.updated:
|
|
133
|
+
(cache hit) executor(clazz, ...)
|
|
134
|
+
Senão:
|
|
135
|
+
script = getScript(trigger) # monta imports + wrapper + getScript()
|
|
136
|
+
clazz = compile(script) # GroovyClassLoader com SecureASTCustomizer
|
|
137
|
+
compiled.put(trigger.id, clazz)
|
|
138
|
+
dates.put(trigger.id, now)
|
|
139
|
+
executor(clazz, ...)
|
|
140
|
+
(se compile falhar → CompilationFailedException é capturada,
|
|
141
|
+
exceptions.add(e.getMessage()), nenhuma classe é cacheada)
|
|
142
|
+
[7] ms = elapsed
|
|
143
|
+
[8] trigger_log.save( new TriggerLog(trigger.id, trigger.id, outputs, exceptions, ms) )
|
|
144
|
+
[9] Retorna Map { trigger, exceptions, outputs, millis }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 2.5 Sub-pipeline — `executor(clazz, ...)`
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
[1] timeout = (trigger.timeout != null && > 0) ? trigger.timeout : 5
|
|
151
|
+
# default 5 segundos; valor literal no código, sem configuração externa
|
|
152
|
+
[2] executor = Executors.newSingleThreadExecutor()
|
|
153
|
+
[3] GroovyObject obj = clazz.newInstance()
|
|
154
|
+
[4] future = executor.submit(callable):
|
|
155
|
+
obj.invokeMethod("setContext", new Object[]{ context })
|
|
156
|
+
obj.invokeMethod("setManager", new Object[]{ manager })
|
|
157
|
+
obj.invokeMethod("trigger", new Object[]{
|
|
158
|
+
trigger.getEvent(), o, player, new GameDao(jongo, manager)
|
|
159
|
+
})
|
|
160
|
+
obj.invokeMethod("addAllOutput", new Object[]{ outputs })
|
|
161
|
+
[5] future.get(timeout, TimeUnit.SECONDS)
|
|
162
|
+
[6] Catches:
|
|
163
|
+
MultipleCompilationErrorsException → exceptions.add; future.cancel(true); executor.shutdownNow
|
|
164
|
+
SecurityException → exceptions.add; future.cancel(true); executor.shutdownNow
|
|
165
|
+
TimeoutException → exceptions.add("Timeout after Xs (timeout allowed Ys)");
|
|
166
|
+
exceptions.add(e.getMessage()); future.cancel(true); shutdownNow
|
|
167
|
+
InterruptedException → exceptions.add(...) (NÃO chama future.cancel — bug latente)
|
|
168
|
+
ExecutionException → exceptions.add; future.cancel(true); shutdownNow
|
|
169
|
+
Exception (catch-all) → exceptions.add; future.cancel(true); shutdownNow
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 2.6 Sub-pipeline — `getScript(trigger)` — wrapper de imports
|
|
173
|
+
|
|
174
|
+
`TriggerRunner.getScript` constrói um script que envelopa o `trigger.script` do usuário em uma classe `FunifierTrigger` Groovy com:
|
|
175
|
+
|
|
176
|
+
1. ~40 imports automáticos (groovyx.net.http, Unirest, Apache HttpClient, Thumbnailator, javamail-simple, todos os domains do Funifier — `Action`, `ActionLog`, `Achievement`, `Lottery`, `Challenge`, `Player`, `Point`, `Team`, `Catalog`, `CatalogItem`, `Notification`, `FunifierMail`, `DateUtil`, `JsonUtil`, `ExcelUtil`, `HttpUtil`, `MustacheUtils`, `ManagerFactory`, `TriggerContext` etc.).
|
|
177
|
+
2. `@TimedInterrupt(value = 200L, unit = TimeUnit.SECONDS)` na classe (limite por thread, distinto do timeout do `future.get`).
|
|
178
|
+
3. Campos `TriggerContext context`, `ManagerFactory manager`, `List output`.
|
|
179
|
+
4. Setters: `setContext(TriggerContext)`, `setManager(ManagerFactory)`.
|
|
180
|
+
5. Override de `println(msg)` → `output.add(msg)` (todos os `println` viram `outputs` no `TriggerLog`).
|
|
181
|
+
6. `addAllOutput(List out)` → `out.addAll(output)`.
|
|
182
|
+
7. O script do usuário (`trigger.getScript()`) — espera uma assinatura `void trigger(event, entity, player, database) { ... }`.
|
|
183
|
+
|
|
184
|
+
Cabeçalho final efetivo do script:
|
|
185
|
+
|
|
186
|
+
```groovy
|
|
187
|
+
@TimedInterrupt(value = 200L, unit = TimeUnit.SECONDS)
|
|
188
|
+
class FunifierTrigger {
|
|
189
|
+
TriggerContext context = null
|
|
190
|
+
ManagerFactory manager = null
|
|
191
|
+
List output = new ArrayList()
|
|
192
|
+
void setContext(TriggerContext c) { context = c }
|
|
193
|
+
void setManager(ManagerFactory c) { manager = c }
|
|
194
|
+
void println(msg) { output.add(msg) }
|
|
195
|
+
void addAllOutput(List out) { out.addAll(output) }
|
|
196
|
+
|
|
197
|
+
// <<< trigger.script é concatenado aqui >>>
|
|
60
198
|
}
|
|
61
199
|
```
|
|
62
200
|
|
|
63
|
-
###
|
|
64
|
-
|
|
65
|
-
|
|
201
|
+
### 2.7 Diagrama — Interação cross-módulo (ActionManager.trackSynchonous)
|
|
202
|
+
|
|
203
|
+
```mermaid
|
|
204
|
+
sequenceDiagram
|
|
205
|
+
autonumber
|
|
206
|
+
participant Caller as Caller (REST)
|
|
207
|
+
participant AM as ActionManager
|
|
208
|
+
participant TM as TriggerManager
|
|
209
|
+
participant SM as StatisticManager
|
|
210
|
+
participant TR as TriggerRunner
|
|
211
|
+
participant Mongo as Mongo (trigger / trigger_log)
|
|
212
|
+
|
|
213
|
+
Caller->>AM: trackSynchonous(actionLog)
|
|
214
|
+
AM->>AM: ctx = new TriggerContext()<br/>ctx.extra["achievements"] = []
|
|
215
|
+
AM->>TM: execute(actionId, actionLog, "action", "before_win", userId, ctx)
|
|
216
|
+
TM->>Mongo: find({entity:"action", event:"before_win"})
|
|
217
|
+
loop cada trigger
|
|
218
|
+
TM->>SM: newTriggerExecution(trigger.id)
|
|
219
|
+
alt limite OK
|
|
220
|
+
TM->>TR: run(trigger, actionLog, userId, manager, ctx)
|
|
221
|
+
TR->>TR: compile (ou cache hit)
|
|
222
|
+
TR->>TR: invokeMethod("trigger", [event,o,player,gameDao])
|
|
223
|
+
TR->>Mongo: trigger_log.save(...)
|
|
224
|
+
else excedeu
|
|
225
|
+
TM->>TM: stdout error (silencia)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
AM->>AM: actionManager.addLog(actionLog)
|
|
229
|
+
AM->>TM: execute(actionId, actionLog, "action", "after_win", userId, ctx)
|
|
230
|
+
AM->>AM: result = achievementManager.fireAction(actionLog)
|
|
231
|
+
AM->>Caller: List<Achievement>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 2.8 Política de timeouts (dupla — relevante para debug)
|
|
235
|
+
|
|
236
|
+
| Limite | Onde está | Valor | O que acontece |
|
|
237
|
+
|---|---|---|---|
|
|
238
|
+
| `trigger.timeout` (segundos) | `TriggerRunner.executor` linha 266–269 | default `5`, sobrescrito pelo campo se `> 0` | `future.get` lança `TimeoutException`, exception registrada no `trigger_log`, `executor.shutdownNow` mata a thread. |
|
|
239
|
+
| `@TimedInterrupt(value = 200L, ...)` | `TriggerRunner.getScript` linha 164 | **200 segundos fixo** | Anotação Groovy que injeta checks em loops/métodos. Trava o script se `200s` excederem mesmo dentro de um único método. **Não respeita `trigger.timeout`** — é um limite teto independente. |
|
|
240
|
+
|
|
241
|
+
Implicação operacional: definir `trigger.timeout = 600` **não vai funcionar**. O `future.get` chega aos `600s` no executor, mas o `@TimedInterrupt` da própria classe Groovy aborta em `200s`. Para todos os efeitos, o teto real é `min(trigger.timeout, 200)`.
|
|
242
|
+
|
|
243
|
+
### 2.9 Cache de classes compiladas
|
|
244
|
+
|
|
245
|
+
`TriggerManager` mantém **dois mapas em memória de processo** (não distribuídos):
|
|
246
|
+
|
|
247
|
+
- `Map<String, Class> compiled` — chave = `trigger.id`, valor = classe Groovy compilada.
|
|
248
|
+
- `Map<String, Date> dates` — chave = `trigger.id`, valor = data da última compilação.
|
|
249
|
+
|
|
250
|
+
Invalidação:
|
|
251
|
+
|
|
252
|
+
| Evento | Efeito |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `add(trigger)` / `update(trigger)` | Faz `compiled.remove(id)` e `dates.remove(id)` após o save. |
|
|
255
|
+
| `delete(id)` | Idem. |
|
|
256
|
+
| `clear(trigger)` (via `PUT /v3/trigger/compile/{id}`) | Idem. |
|
|
257
|
+
| `PUT /v3/trigger/compile/all` | Itera todos e chama `clear` em cada um. |
|
|
258
|
+
| `TriggerRunner.run` (cache hit) | Reutiliza a classe **apenas se** `dates.get(id).getTime() > trigger.updated.getTime()`. Caso contrário, recompila. |
|
|
259
|
+
|
|
260
|
+
**Cluster awareness:** o comentário `//cluster:...` no código indica que essa lógica foi feita para um ambiente multi-nó — cada nó tem seu próprio cache, e a comparação `dates > updated` recompila localmente quando um nó vizinho atualizou o trigger no Mongo.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 3. Estrutura dos Objetos
|
|
265
|
+
|
|
266
|
+
### 3.1 `Trigger` — documento raiz (`trigger`)
|
|
267
|
+
|
|
268
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
269
|
+
|---|---|---|---|---|
|
|
270
|
+
| `_id` | String | `Guid.shortTimeMillis()` no `add` se ausente | — | Identificador único. |
|
|
271
|
+
| `name` | String | — | recomendado | Rótulo legível. Usado nos filtros `findAll`. |
|
|
272
|
+
| `description` | String | — | não | Texto livre. **Não é validado/indexado**. |
|
|
273
|
+
| `entity` | String | — | **sim** | Nome da coleção/entidade alvo (ex: `action`, `point_category`, `challenge`, `level`, `lottery_ticket`, `swap`, `mystery_box`, ou qualquer string usada por chamadores customizados como `"deal"`, `"folder_progress"`). |
|
|
274
|
+
| `event` | String | — | **sim** | Tipo de evento (ver enums abaixo + tabela 3.4). |
|
|
275
|
+
| `script` | String | — | **sim** | Código Groovy do corpo. Espera definir `void trigger(event, entity, player, database) { ... }`. |
|
|
276
|
+
| `creation` | Date | `new Date()` no `add` se ausente | — | Timestamp de criação. |
|
|
277
|
+
| `updated` | Date | `new Date()` em todo `add`/`update` | — | Usado para invalidar cache em cluster. Sobrescreve `creation`-like updates. |
|
|
278
|
+
| `timeout` | Long | `null` → cai para `5` segundos no executor | não | Timeout de execução em **segundos**. Teto real efetivo `min(timeout, 200)` por causa do `@TimedInterrupt`. |
|
|
279
|
+
| `techniques` | `List<String>` | `null` | não | Lista de códigos de técnica de jogo (GT...) para metadata no Studio. **Apenas persiste — não é lida pelo runtime.** |
|
|
280
|
+
| `style` | `ObjectStyle` | `null` | não | Atributos visuais (`background`, `visible`). Usado apenas pelo Studio. |
|
|
281
|
+
| `references` | `List<Reference>` | `null` | não | Vínculo visual com outros itens da estratégia (consumido por relatórios do Studio). |
|
|
282
|
+
| `errors` | `String[]` | `null` | — | **Campo legado** — escrito apenas pelo método comentado `SaveTrigger(...)` que não é mais chamado. Permanece no schema (com getter/setter) mas nunca é populado no fluxo atual. |
|
|
283
|
+
| `logs` | `String[]` | `null` | — | **Campo legado** — mesma situação de `errors`. O log moderno vai para a coleção `trigger_log`. |
|
|
284
|
+
|
|
285
|
+
Configuração de serialização:
|
|
286
|
+
|
|
287
|
+
- `@JsonIgnoreProperties(ignoreUnknown=true)` — qualquer campo extra enviado no JSON é **silenciosamente descartado**.
|
|
288
|
+
- `@JsonProperty("_id")` no `id` — o JSON da API expõe `_id`.
|
|
289
|
+
- `JsonUtil.toJsonRemoveNullFields` nos endpoints — campos `null` (`description`, `creation`, `errors`, `logs`, `style`, `references`, `techniques`, `timeout`, `updated`) **não aparecem no JSON de resposta**, mesmo que estejam no documento Mongo.
|
|
290
|
+
|
|
291
|
+
#### Campos legados (aceitos mas sem efeito runtime)
|
|
292
|
+
|
|
293
|
+
- `Trigger.errors`, `Trigger.logs` — getters/setters existem; apenas o método privado `SaveTrigger(...)` (comentado entre linhas 540–551 de `TriggerRunner.java`) os populava. Hoje, todo log de execução vai para `trigger_log`.
|
|
294
|
+
|
|
295
|
+
#### Constantes de Entity (em `Trigger.java`)
|
|
296
|
+
|
|
297
|
+
```java
|
|
298
|
+
ENTITY_ACTION = "action"
|
|
299
|
+
ENTITY_PLAYER = "player"
|
|
300
|
+
ENTITY_WIN_STATE = "challenge"
|
|
301
|
+
ENTITY_POINT = "point_category"
|
|
302
|
+
ENTITY_LEVEL = "level"
|
|
303
|
+
ENTITY_CROWN = "crown"
|
|
304
|
+
ENTITY_CATALOG_ITEM = "catalog_item"
|
|
305
|
+
ENTITY_LOTTERY = "lottery"
|
|
306
|
+
ENTITY_CHARACTER_STAR_STATS = "character_star_stats_level"
|
|
307
|
+
ENTITY_UPLOAD = "upload"
|
|
308
|
+
ENTITY_MYSTERY_BOX = "mystery_box"
|
|
309
|
+
|
|
310
|
+
// Comentadas/removidas (não aceitar como valor canônico):
|
|
311
|
+
// ENTITY_ACTIONLOG = "action_log"
|
|
312
|
+
// ENTITY_ACHIEVEMENT = "achievement"
|
|
313
|
+
// ENTITY_COMPETITION = "competition"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Importante: o campo `entity` aceita **qualquer string** — não há validação. Strings observadas no código fora dessas constantes:
|
|
317
|
+
|
|
318
|
+
- `"swap"`, `"swap_counter_offer"` — disparado por `SwapManager`.
|
|
319
|
+
- `"folder_log"`, `"folder_progress"` — `FolderManager` / `FolderRest`.
|
|
320
|
+
- `"quiz"`, `"quiz_log"`, `"question"`, `"question_log"` — `QuizManager`, `QuestionManager`.
|
|
321
|
+
- `"deal"` — `CrmManager` (módulo CRM B2B).
|
|
322
|
+
- `"compact_log"`, `"backup_log"` — `CompactManager`, `BackupManager`.
|
|
323
|
+
- `"password_change"` — `PlayerRest.changePassword`.
|
|
324
|
+
- Qualquer string de coleção passada por `DatabaseRest`/`DatabaseManager` (no `before_create`/`after_create`/`before_update`/`after_update`/`before_delete`/`after_delete`/`before_bulk`/`after_bulk`).
|
|
325
|
+
|
|
326
|
+
#### Constantes de Event (em `Trigger.java`)
|
|
327
|
+
|
|
328
|
+
```java
|
|
329
|
+
EVENT_BEFORE_WIN = "before_win"
|
|
330
|
+
EVENT_AFTER_WIN = "after_win"
|
|
331
|
+
EVENT_BEFORE_LOSE = "before_lose"
|
|
332
|
+
EVENT_AFTER_LOSE = "after_lose"
|
|
333
|
+
EVENT_BEFORE_CREATE = "before_create"
|
|
334
|
+
EVENT_AFTER_CREATE = "after_create"
|
|
335
|
+
EVENT_BEFORE_UPDATE = "before_update"
|
|
336
|
+
EVENT_AFTER_UPDATE = "after_update"
|
|
337
|
+
EVENT_BEFORE_DELETE = "before_delete"
|
|
338
|
+
EVENT_AFTER_DELETE = "after_delete"
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Eventos extras NÃO declarados como constante mas usados ad-hoc:
|
|
342
|
+
|
|
343
|
+
- `"before_bulk"`, `"after_bulk"` — `DatabaseManager.bulkInsert`.
|
|
344
|
+
- `"before_purchase_validation"` — `CatalogManager.purchase`.
|
|
345
|
+
- `"before_acquire_validation"` — `SwapManager.buyerAcquire`.
|
|
346
|
+
- `"before_win_reward"` — `MysteryBoxManager.execute` (dispara antes de cada reward individual de uma caixa).
|
|
347
|
+
- `"after_finish"` — `CompactManager.asyncExecute`.
|
|
66
348
|
|
|
67
|
-
|
|
349
|
+
### 3.2 `Reference` — subdocumento (`references[]`)
|
|
68
350
|
|
|
69
|
-
|
|
351
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
352
|
+
|---|---|---|---|---|
|
|
353
|
+
| `_id` | String | — | sim | Id do item referenciado. |
|
|
354
|
+
| `type` | String | — | sim | Tipo/coleção (ex: `Entity.POINT.collection`). |
|
|
355
|
+
| `title` | String | — | sim | Texto exibido no card visual do Studio. |
|
|
356
|
+
| `way` | String | — | sim | `"from"` ou `"to"` (`Reference.WAY_FROM` / `WAY_TO`). |
|
|
357
|
+
| `linkLabel` | String | `""` (via `getLinkLabel()`) | não | Rótulo sobre a seta no diagrama de estratégia do Studio. |
|
|
70
358
|
|
|
71
|
-
|
|
359
|
+
Apenas metadata para o Studio — **não é lida pelo runtime do trigger**.
|
|
72
360
|
|
|
73
|
-
|
|
74
|
-
2. Se precisar de uma classe não importada, use o nome completo: `com.example.MinhaClasse`
|
|
75
|
-
3. **`manager`** (ManagerFactory) está disponível como campo da classe
|
|
76
|
-
4. **Timeout padrão de 10 segundos** — pode ser customizado via campo `timeout` (em **segundos**) na API: `POST /v3/trigger` com `{"_id": "trigger_id", "timeout": 30}`. Este campo não aparece no Studio.
|
|
77
|
-
5. Scripts rodam em **Groovy** — cuidado com `$` em GStrings (usar `String.valueOf((char)0x24)` para operadores MongoDB)
|
|
78
|
-
6. Use apenas **ASCII em comentários** — UTF-8 especial pode causar parse errors
|
|
361
|
+
### 3.3 `ObjectStyle` — subdocumento (`style`)
|
|
79
362
|
|
|
80
|
-
|
|
363
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
364
|
+
|---|---|---|---|---|
|
|
365
|
+
| `background` | String | — | não | Cor de fundo no Studio. |
|
|
366
|
+
| `visible` | Boolean | `null` | não | Se `false`, o item é ocultado nas visualizações do Studio. |
|
|
81
367
|
|
|
82
|
-
|
|
368
|
+
### 3.4 `TriggerLog` — documento de log (`trigger_log`)
|
|
83
369
|
|
|
84
|
-
|
|
85
|
-
- `entity` (Object): objeto sendo manipulado (Player, ActionLog, Achievement, etc.)
|
|
86
|
-
- `player` (String): ID do jogador
|
|
87
|
-
- `database` (Object): utilitário para acessar o banco de dados
|
|
370
|
+
Gerado em **toda** execução pelo `TriggerRunner.run`, inclusive em caso de falha de compilação ou timeout (carrega o erro em `err`).
|
|
88
371
|
|
|
89
|
-
|
|
372
|
+
| Campo | Tipo | Padrão | Descrição |
|
|
373
|
+
|---|---|---|---|
|
|
374
|
+
| `_id` | String | `Guid.newShortGuid()` no `addLog` se ausente | Id do log. |
|
|
375
|
+
| `item` | String | — | **No fluxo atual = `trigger.id`** (o `TriggerLog` é construído como `new TriggerLog(trigger.id, trigger.id, ...)`). O nome do campo sugere "id do item disparado" mas hoje duplica o `trigger.id`. |
|
|
376
|
+
| `time` | Date | `new Date()` no construtor + `new Date()` no `addLog` se null | Quando o log foi registrado. |
|
|
377
|
+
| `out` | `List<String>` | `null` se vazio | Captura de todos os `println` do script. **Se a lista chegar vazia (`size == 0`), é gravada como `null`** (linha 37 de `TriggerLog.java`). |
|
|
378
|
+
| `err` | `List<String>` | `null` se vazio | Stack messages das exceções (compilação, timeout, security, etc.). Mesma regra de `out`: vazio → `null`. |
|
|
379
|
+
| `ms` | long | — | Tempo total de execução em milissegundos (medido do início do `run` até depois do `addLog`). |
|
|
90
380
|
|
|
91
|
-
|
|
381
|
+
### 3.5 Subentidade — `TriggerContext` (runtime, não persistido)
|
|
92
382
|
|
|
93
|
-
|
|
383
|
+
Não é persistido no Mongo. É construído ad-hoc pelo manager chamador e injetado no script via `setContext`.
|
|
384
|
+
|
|
385
|
+
| Campo | Tipo | Uso |
|
|
386
|
+
|---|---|---|
|
|
387
|
+
| `manager` | `ManagerFactory` | Acesso a todos os managers da plataforma para o tenant. |
|
|
388
|
+
| `origin` | Object | Convenção: a entidade que originou a cadeia de eventos (ex: o `ActionLog` original quando o evento atual é um `point_category.after_win` derivado). |
|
|
389
|
+
| `extra` | `Map<String, Object>` | Bag de dados arbitrários. Chaves observadas no código: `"achievements"` (lista mutável em que o script pode adicionar Achievements), `"parentActionLog"`, `"deal"`, `"level"`. |
|
|
390
|
+
|
|
391
|
+
### 3.6 `TriggerStatistics` / `TriggerDetailStatistics` (estatísticas in-memory)
|
|
392
|
+
|
|
393
|
+
Não são persistidos por trigger. Vivem dentro do `StatisticManager` por `apiKey` e são serializados periodicamente como `Entity.STATISTIC_LOG`.
|
|
394
|
+
|
|
395
|
+
| Campo | Tipo | Descrição |
|
|
94
396
|
|---|---|---|
|
|
95
|
-
| `
|
|
96
|
-
|
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
397
|
+
| `lastTriggerExecution` | Date | Última execução de qualquer trigger. |
|
|
398
|
+
| `hourlyTriggerExecutions` | long | Contador da hora atual (reset ao trocar de hora). |
|
|
399
|
+
| `dailyTriggerExecutions` | long | Contador do dia atual (reset ao trocar de dia do ano). |
|
|
400
|
+
| `monthlyTriggerExecutions` | long | Contador do mês atual (reset ao trocar de mês). |
|
|
401
|
+
| `dailyTriggerExecutionsLimit` | long | Limite diário (`@JsonIgnore` — não vaza pelo JSON). Carregado de `GamificationLimits.dailyTriggerExecutions`. |
|
|
402
|
+
| `triggers` | `Map<String, TriggerDetailStatistics>` | Detalhe por `trigger.id`. |
|
|
403
|
+
|
|
404
|
+
Lógica de reset (linha 57–78 de `TriggerStatistics.java`): compara hora/dia/mês atuais com os armazenados — quando muda, o contador é **resetado para 1** (não 0). Logo, no primeiro tick de cada dia o contador começa em 1.
|
|
99
405
|
|
|
100
|
-
|
|
101
|
-
- ❌ `entity.get("_id")` em trigger de `player` → `MissingMethodException: No signature of method: Player.get()`
|
|
102
|
-
- ✅ `entity.id` em trigger de `player`
|
|
103
|
-
- ❌ `entity.id` em trigger de `signup__c` → campo não existe no Map
|
|
104
|
-
- ✅ `entity.get("_id")` em trigger de `signup__c`
|
|
406
|
+
Decisão de bloqueio (linha 92–98):
|
|
105
407
|
|
|
106
|
-
|
|
408
|
+
```java
|
|
409
|
+
if (dailyTriggerExecutions <= dailyTriggerExecutionsLimit) return true;
|
|
410
|
+
else return false;
|
|
107
411
|
```
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
412
|
+
|
|
413
|
+
Observação: o teste é `<=`, então o limite é **inclusivo** — quando `daily == limit`, ainda passa; bloqueia a partir de `daily > limit`.
|
|
414
|
+
|
|
415
|
+
### 3.7 `TriggerHtml` — **NÃO faz parte do runtime de Trigger**
|
|
416
|
+
|
|
417
|
+
Apesar do nome, `TriggerHtml` (coleção `trigger_html`) é um modelo client-side de eventos DOM do SDK web. Está em `engine/integration/html/TriggerHtml.java` e é gerenciado pelo `ActionDaoMongo` (não pelo `TriggerManager`). Existe documentação separada — não confundir.
|
|
418
|
+
|
|
419
|
+
### 3.8 Técnicas de jogo (`techniques`)
|
|
420
|
+
|
|
421
|
+
Apenas armazenamento — o array de códigos GT (ex: `["GT001", "GT042"]`) é persistido e devolvido pela API mas **não influencia o runtime**. Usado pelo Studio para taggear visualmente os triggers em relatórios de cobertura de técnicas.
|
|
422
|
+
|
|
423
|
+
### 3.9 Ciclo de vida do `Trigger` no cache
|
|
424
|
+
|
|
425
|
+
```mermaid
|
|
426
|
+
stateDiagram-v2
|
|
427
|
+
[*] --> Persistido: add(trigger)<br/>updated = now
|
|
428
|
+
Persistido --> Compilado: TriggerRunner.run<br/>compile + cache
|
|
429
|
+
Compilado --> Compilado: run reutiliza cache<br/>dates > updated
|
|
430
|
+
Compilado --> Persistido: trigger.updated alterado<br/>(outro nó editou)
|
|
431
|
+
Compilado --> Invalidado: update(trigger)<br/>delete(trigger)<br/>clear(trigger)
|
|
432
|
+
Invalidado --> Compilado: próxima run recompila
|
|
433
|
+
Persistido --> [*]: delete(id)
|
|
116
434
|
```
|
|
117
435
|
|
|
118
|
-
|
|
436
|
+
---
|
|
119
437
|
|
|
120
|
-
##
|
|
438
|
+
## 4. Endpoints
|
|
121
439
|
|
|
122
|
-
|
|
440
|
+
### 4.1 `GET /v3/trigger` — listar com filtros
|
|
123
441
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- **Tempo de resposta**: tempo de execução em ms
|
|
130
|
-
- **Erros**: se houve erro, mostra a exception completa em vermelho
|
|
131
|
-
4. Se não houver seção "Erros", a trigger executou com sucesso
|
|
442
|
+
| Aspecto | Detalhe |
|
|
443
|
+
|---|---|
|
|
444
|
+
| Finalidade | Listar/filtrar triggers via aggregation. |
|
|
445
|
+
| Autenticação | Bearer token. |
|
|
446
|
+
| Tipo | Read. |
|
|
132
447
|
|
|
133
|
-
**
|
|
448
|
+
**Query params:**
|
|
134
449
|
|
|
135
|
-
|
|
450
|
+
| Param | Tipo | Descrição |
|
|
451
|
+
|---|---|---|
|
|
452
|
+
| `id` | String | Filtro exato por `_id`. |
|
|
453
|
+
| `name` | String | Filtro `$regex` case-insensitive (`{$regex: <name>, $options: 'i'}`). |
|
|
454
|
+
| `entity` | String | Filtro exato. |
|
|
455
|
+
| `event` | String | Filtro exato. |
|
|
456
|
+
| `script` | String | Filtro `$regex` case-insensitive no corpo do script. |
|
|
457
|
+
| `q` | String | **Fragmento bruto** de query Mongo, concatenado no `$match` (ex: `q=tags:{$all:["x"]}`). Sem sanitização — ver seção 8. |
|
|
458
|
+
| `fields` | String CSV | Projeção (`{$project:{a:1,b:1}}`). |
|
|
459
|
+
| `orderby` | String | Campo de ordenação. |
|
|
460
|
+
| `reverse` | boolean (string) | `true` → `-1`, qualquer outro valor → `1`. |
|
|
461
|
+
| `max_results` | int (string) | Limite. Se `<= 0` → default **`100`**. |
|
|
462
|
+
|
|
463
|
+
**Comportamento real:**
|
|
464
|
+
|
|
465
|
+
- `max_results` é sempre clamped para `100` quando `<= 0` (linha 149 do `TriggerManager.findAllTriggers`).
|
|
466
|
+
- Argumentos vazios/`null` são ignorados sem warning.
|
|
467
|
+
- `name` e `script` aceitam **regex livre** (não há escape de metacaracteres).
|
|
468
|
+
- `q` é literalmente concatenado dentro de `{ $match: { ..., <q> } }` — **injeção de query Mongo é possível**.
|
|
469
|
+
|
|
470
|
+
**Exemplo:**
|
|
471
|
+
|
|
472
|
+
```http
|
|
473
|
+
GET /v3/trigger?entity=action&event=after_win&max_results=50
|
|
474
|
+
Authorization: Bearer eyJ...
|
|
475
|
+
```
|
|
136
476
|
|
|
137
|
-
|
|
477
|
+
```json
|
|
478
|
+
[
|
|
479
|
+
{
|
|
480
|
+
"_id": "5f3a1b...",
|
|
481
|
+
"name": "Notificar Slack após sell",
|
|
482
|
+
"entity": "action",
|
|
483
|
+
"event": "after_win",
|
|
484
|
+
"script": "void trigger(event, entity, player, database){ ... }",
|
|
485
|
+
"creation": "2024-08-01T13:21:09Z",
|
|
486
|
+
"updated": "2024-08-12T17:02:55Z",
|
|
487
|
+
"timeout": 10
|
|
488
|
+
}
|
|
489
|
+
]
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 4.2 `GET /v3/trigger/{id}` — buscar por id
|
|
493
|
+
|
|
494
|
+
| Aspecto | Detalhe |
|
|
495
|
+
|---|---|
|
|
496
|
+
| Finalidade | Retorna um único trigger por `_id`. |
|
|
497
|
+
| Autenticação | Bearer token. |
|
|
498
|
+
| Comportamento se não encontrado | `findById` retorna `null`; resposta é `"null"` literal (após `toJsonRemoveNullFields`). **Não retorna 404.** |
|
|
499
|
+
|
|
500
|
+
### 4.3 `POST /v3/trigger/execute/{id}?player=<player>` — debug execution
|
|
501
|
+
|
|
502
|
+
| Aspecto | Detalhe |
|
|
138
503
|
|---|---|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
| `new Date().getTime()` | Timestamp atual em milissegundos |
|
|
144
|
-
| `manager` | ManagerFactory — acesso a todos os managers |
|
|
504
|
+
| Finalidade | Executar um trigger isolado para debug. |
|
|
505
|
+
| Autenticação | Bearer token. |
|
|
506
|
+
| Body | JSON livre (`Map<String, Object>`) — passado como `o` ao script. |
|
|
507
|
+
| Tipo | Execução side-effectful. |
|
|
145
508
|
|
|
146
|
-
|
|
509
|
+
**Comportamento real:**
|
|
147
510
|
|
|
148
|
-
|
|
511
|
+
- **Não chama `TriggerManager.execute`** — instancia um `new TriggerRunner()` e chama `run(trigger, o, player, manager, null)` diretamente.
|
|
512
|
+
- **NÃO valida `dailyTriggerExecutionsLimit`** (pula `StatisticManager.newTriggerExecution`).
|
|
513
|
+
- `context` é `null` — qualquer script que faça `context.extra.put(...)` lança NPE.
|
|
514
|
+
- Body é passado ao script como o objeto `o` da assinatura `trigger(event, entity, player, database)`.
|
|
149
515
|
|
|
150
|
-
|
|
151
|
-
- `manager.getActionManager()` - findActionById, track, trackSynchonous
|
|
152
|
-
- `manager.getCatalogManager()` - findItemById, purchase, undoPurchase
|
|
153
|
-
- `manager.getLotteryManager()` - find, insertTicket, execute
|
|
154
|
-
- `manager.getAchievementManager()` - addAchievement
|
|
155
|
-
- `manager.getJongoConnection()` - acesso direto ao MongoDB
|
|
516
|
+
**Resposta:**
|
|
156
517
|
|
|
157
|
-
|
|
518
|
+
```json
|
|
519
|
+
{
|
|
520
|
+
"trigger": { ... documento completo ... },
|
|
521
|
+
"exceptions": [],
|
|
522
|
+
"outputs": ["valor que veio do println do script"],
|
|
523
|
+
"millis": 47
|
|
524
|
+
}
|
|
525
|
+
```
|
|
158
526
|
|
|
159
|
-
|
|
527
|
+
Se a trigger não existir, o endpoint passa `null` para `TriggerRunner.run` que faz NPE em `trigger.updated` — retorna 500.
|
|
160
528
|
|
|
161
|
-
###
|
|
162
|
-
- `EmailBuilder` / `MailerBuilder` — Simple Java Mail (construir e enviar)
|
|
163
|
-
- `com.funifier.engine.mail.MailContext` — config SMTP da gamificação
|
|
164
|
-
- `com.funifier.controller.Configuration` — acesso à configuração atual
|
|
165
|
-
- `com.funifier.engine.util.MustacheUtils` — parse de templates com variáveis `{{campo}}`
|
|
529
|
+
### 4.4 `POST /v3/trigger` — criar/atualizar
|
|
166
530
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
531
|
+
| Aspecto | Detalhe |
|
|
532
|
+
|---|---|
|
|
533
|
+
| Finalidade | Criar/sobrescrever trigger. |
|
|
534
|
+
| Autenticação | Bearer token. |
|
|
535
|
+
| Full replace ou patch | **Full replace.** O `_id`, se informado, sobrescreve o documento existente (Jongo `save`). Não é patch. |
|
|
536
|
+
|
|
537
|
+
**Comportamento real (`TriggerRest.insert`):**
|
|
538
|
+
|
|
539
|
+
1. Se o body for `null`, devolve `201 Created` com `{}` — não erro.
|
|
540
|
+
2. Chama `TriggerManager.compile(trigger)` — só valida sintaxe + AST (não roda).
|
|
541
|
+
3. Se `CompilationFailedException` / `InstantiationException` / `IllegalAccessException`:
|
|
542
|
+
```json
|
|
543
|
+
{ "trigger": { ... }, "status": "ERROR", "message": "<msg>" }
|
|
544
|
+
```
|
|
545
|
+
**HTTP status continua 201 Created** mesmo com erro de compilação.
|
|
546
|
+
4. Se OK, chama `TriggerManager.add(trigger)`:
|
|
547
|
+
- Preenche `_id` se vazio (`Guid.shortTimeMillis()`).
|
|
548
|
+
- Preenche `creation` se null.
|
|
549
|
+
- **Sempre sobrescreve `updated = new Date()`**.
|
|
550
|
+
- `c.save(trigger)` — UPSERT por `_id`.
|
|
551
|
+
- Invalida cache local: `compiled.remove(id)` + `dates.remove(id)`.
|
|
552
|
+
5. Retorna `{ "trigger": <persisted>, "status": "OK" }` com HTTP 201.
|
|
553
|
+
|
|
554
|
+
**Exemplo:**
|
|
175
555
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
556
|
+
```json
|
|
557
|
+
POST /v3/trigger
|
|
558
|
+
Authorization: Bearer eyJ...
|
|
559
|
+
Content-Type: application/json
|
|
560
|
+
|
|
561
|
+
{
|
|
562
|
+
"name": "After Register Action Log",
|
|
563
|
+
"entity": "action",
|
|
564
|
+
"event": "after_win",
|
|
565
|
+
"timeout": 10,
|
|
566
|
+
"script": "void trigger(event, entity, player, database) {\n println 'event:' + event + ', player:' + player\n}"
|
|
567
|
+
}
|
|
180
568
|
```
|
|
181
569
|
|
|
182
|
-
|
|
570
|
+
### 4.5 `DELETE /v3/trigger/{id}`
|
|
571
|
+
|
|
572
|
+
| Aspecto | Detalhe |
|
|
573
|
+
|---|---|
|
|
574
|
+
| Finalidade | Remover trigger + invalidar cache. |
|
|
575
|
+
| Autenticação | Bearer token. |
|
|
576
|
+
| Resposta | `204 No Content`. |
|
|
577
|
+
|
|
578
|
+
**Comportamento real:** `mongo.remove({_id:#})` + `compiled.remove(id)` + `dates.remove(id)`. Se o id não existir, é no-op silencioso (Mongo não retorna erro).
|
|
579
|
+
|
|
580
|
+
### 4.6 `PUT /v3/trigger/compile/{id}` — invalidar cache
|
|
581
|
+
|
|
582
|
+
| Aspecto | Detalhe |
|
|
583
|
+
|---|---|
|
|
584
|
+
| Finalidade | Forçar recompilação **na próxima execução**. |
|
|
585
|
+
| Autenticação | Bearer token. |
|
|
586
|
+
| Valor especial `id` | `"all"` itera todos os triggers e chama `clear` em cada um. |
|
|
587
|
+
|
|
588
|
+
**Comportamento real:** apesar do nome, **não compila nada agora** — apenas remove o `id` dos mapas `compiled` e `dates`. A próxima execução real é que vai recompilar.
|
|
183
589
|
|
|
184
|
-
|
|
590
|
+
**Resposta:**
|
|
591
|
+
|
|
592
|
+
```json
|
|
593
|
+
{ "_id": "all" }
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
ou
|
|
597
|
+
|
|
598
|
+
```json
|
|
599
|
+
{ "_id": "5f3a1b..." }
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Se o id passado não existir, `findById` retorna `null`, o trecho `if(trigger != null)` é falso e o endpoint ainda devolve `200 OK` com `_id` ecoado. **Não retorna 404.**
|
|
603
|
+
|
|
604
|
+
### 4.7 Endpoints inexistentes (esperados em REST convencional, ausentes aqui)
|
|
605
|
+
|
|
606
|
+
- Não há `PUT /v3/trigger/{id}` para update — usar `POST` com `_id` no body.
|
|
607
|
+
- Não há `GET /v3/trigger/{id}/logs` — para ver logs, consultar a coleção `trigger_log` via `/v3/database/trigger_log`.
|
|
608
|
+
- Não há paginação cursor-based — apenas `max_results` clamped a 100 por default.
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## 5. Regras de Negócio
|
|
613
|
+
|
|
614
|
+
- **`(entity, event)` é a chave de roteamento.** Não há regra adicional de tenant filter — todos os triggers daquela combinação rodam.
|
|
615
|
+
- **Ordem de execução não é determinística.** `findByEntityAndEvent` itera o cursor Mongo na ordem natural (geralmente ordem de inserção em RocksDB/WiredTiger, mas não garantida).
|
|
616
|
+
- **Isolamento por tenant é via `apiKey`.** Cada `ManagerFactory` tem sua própria conexão Mongo (banco do tenant) → `findByEntityAndEvent` só vê triggers daquele tenant.
|
|
617
|
+
- **Limite diário (`dailyTriggerExecutionsLimit`)**:
|
|
618
|
+
- Vem de `GamificationLimits.dailyTriggerExecutions` (coleção `game_limits`).
|
|
619
|
+
- Se `0`, **bloqueia tudo** (porque `1 <= 0` é falso).
|
|
620
|
+
- Quando excedido, a execução é **silenciosamente pulada** — `System.out.println` de erro no stdout do servidor; nenhum `trigger_log` é gravado.
|
|
621
|
+
- Contagem é **in-memory**: se o serviço reinicia, o contador zera.
|
|
622
|
+
- **`before_*` e `after_*` rodam em volta da mutação principal**, **na mesma thread chamadora**. Logo, um `before_create` lento atrasa o `insert` HTTP. Um `before_create` que joga exception é **engolido** (`e.printStackTrace`) e a operação continua — não é uma forma de cancelar a operação.
|
|
623
|
+
- **Não há rollback transacional** entre o script e o save do entity. Se o `after_create` falha, o documento já está persistido.
|
|
624
|
+
- **`script` recebido na API é compilado mas não é checado para a assinatura `trigger(event, entity, player, database)`.** Se faltar essa função, o `invokeMethod("trigger", ...)` lança `MissingMethodException` apenas em runtime, registrada em `trigger_log.err`.
|
|
625
|
+
- **Triggers customizadas podem disparar outros eventos**: se um script chama `manager.getActionManager().trackSynchonous(...)`, isso dispara novas triggers — **risco de loop infinito**, limitado apenas pelos timeouts e pelo `dailyTriggerExecutionsLimit`.
|
|
626
|
+
- **`techniques`, `style`, `references` são apenas metadados de Studio** — runtime ignora.
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## 6. Comportamentos Automáticos
|
|
631
|
+
|
|
632
|
+
| Comportamento | Trigger (gatilho) | Impacto | Persistência |
|
|
633
|
+
|---|---|---|---|
|
|
634
|
+
| Atribuição de `_id` | `add(trigger)` sem `_id` | Gera `Guid.shortTimeMillis()`. | Sim — gravado. |
|
|
635
|
+
| Atribuição de `creation` | `add(trigger)` sem `creation` | `new Date()`. | Sim. |
|
|
636
|
+
| Atualização de `updated` | Todo `add`/`update` | Sobrescreve com `new Date()`. | Sim. Não respeita valor enviado pelo cliente. |
|
|
637
|
+
| Auto-save no primeiro run | `TriggerRunner.run` quando `trigger.updated == null` | Persiste o trigger no Mongo. | Sim — efeito colateral. |
|
|
638
|
+
| Invalidação de cache | `add` / `update` / `delete` / `clear` | `compiled.remove(id)` + `dates.remove(id)` apenas no processo local. | Não — só memória. |
|
|
639
|
+
| Recompilação por staleness | `run` detecta `trigger.updated > dates.get(id)` | Recompila e re-cacheia. | Não. |
|
|
640
|
+
| Log de execução | Toda chamada de `TriggerRunner.run` (independente de sucesso) | Insert em `trigger_log` com `out`, `err`, `ms`. | Sim, sempre. |
|
|
641
|
+
| Estatística | Toda chamada de `TriggerManager.execute` antes do run | Incremento de contadores em memória + decisão de limite. | Não direto — `StatisticManager` flushea em ciclos próprios. |
|
|
642
|
+
| Println capturado | Toda chamada de `println` no script | Adiciona a `output` (lista interna). | Sim, vai para `trigger_log.out`. |
|
|
643
|
+
| Wrapping de classes | Toda execução, antes da compilação | Adiciona ~40 imports + classe `FunifierTrigger` + setters. | Não — só em memória. |
|
|
644
|
+
| Reset diário/horário/mensal | `newTriggerExecution` em qualquer execução | Quando o `Calendar.HOUR_OF_DAY` / `DAY_OF_YEAR` / `MONTH` muda, o contador correspondente vira `1`. | Não diretamente. |
|
|
645
|
+
|
|
646
|
+
### 6.1 Diagrama — Behaviors encadeados em `POST /v3/trigger`
|
|
647
|
+
|
|
648
|
+
```mermaid
|
|
649
|
+
flowchart LR
|
|
650
|
+
A[POST /v3/trigger] --> B[TriggerRest.insert]
|
|
651
|
+
B --> C[manager.compile<br/>valida AST]
|
|
652
|
+
C -->|fail| F[status=ERROR<br/>HTTP 201 retorna]
|
|
653
|
+
C -->|OK| D[manager.add]
|
|
654
|
+
D --> E1[_id := Guid.shortTimeMillis<br/>se vazio]
|
|
655
|
+
D --> E2[creation := now<br/>se null]
|
|
656
|
+
D --> E3[updated := now<br/>sempre]
|
|
657
|
+
D --> E4[c.save trigger<br/>UPSERT por _id]
|
|
658
|
+
D --> E5[compiled.remove id<br/>dates.remove id]
|
|
659
|
+
E5 --> G[HTTP 201 + status=OK]
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
## 7. Suportado vs NÃO Suportado
|
|
665
|
+
|
|
666
|
+
### ✅ Suportado
|
|
667
|
+
|
|
668
|
+
- CRUD básico via `/v3/trigger` (criar, listar, buscar, deletar).
|
|
669
|
+
- Listagem com `$regex` em `name`/`script` e `$match` livre via `q`.
|
|
670
|
+
- Compilação `SecureASTCustomizer` com whitelist de tokens (operadores aritméticos, comparações, lógicos, `[`/`]`).
|
|
671
|
+
- Bloqueio de classes perigosas (`System`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject`) e expressões (`.execute`, `.getDB`, `.getMongo`, `.dropDatabase`) via `TriggerExpressionChecker`.
|
|
672
|
+
- Cache local de classes Groovy compiladas (per processo, invalidado por update/delete).
|
|
673
|
+
- Timeout configurável por trigger (efetivo até 200s).
|
|
674
|
+
- Endpoint de execução para debug (`POST /v3/trigger/execute/{id}`).
|
|
675
|
+
- Endpoint para forçar invalidação de cache (`PUT /v3/trigger/compile/{id}`, `compile/all`).
|
|
676
|
+
- Captura de `println` em `trigger_log.out`.
|
|
677
|
+
- Captura de exceções em `trigger_log.err`.
|
|
678
|
+
- Disparo via 29 managers/REST distintos, em todos os eventos `before_*` / `after_*` documentados em 3.1.
|
|
679
|
+
- `TriggerContext.extra["achievements"]` como canal de "saída" do script para o manager chamador (especialmente `AchievementManager`).
|
|
680
|
+
- Estatísticas in-memory de execução por hora/dia/mês (`TriggerStatistics`).
|
|
681
|
+
- Limite diário aplicado por tenant (`GamificationLimits.dailyTriggerExecutions`).
|
|
682
|
+
|
|
683
|
+
### ❌ NÃO Suportado / Comportamento limitado
|
|
684
|
+
|
|
685
|
+
- **Sem `PUT /v3/trigger/{id}`** — atualização é feita via `POST` com `_id` no body (mesmo endpoint do create). O endpoint `PUT /v3/trigger/compile/{id}` não é um PUT de update, é apenas invalidação de cache.
|
|
686
|
+
- **404 não é retornado** para id inexistente em `GET /v3/trigger/{id}` nem `PUT /v3/trigger/compile/{id}`.
|
|
687
|
+
- **HTTP status não reflete erro de compilação** — `POST /v3/trigger` devolve `201 Created` mesmo quando `compile` falha (o erro vem no body `status: "ERROR"`).
|
|
688
|
+
- **Sem rollback transacional** — se um `after_create` falha, a entidade já foi criada.
|
|
689
|
+
- **`techniques`, `style`, `references` não influenciam runtime** — são apenas metadata de Studio. Aceitos no schema, ignorados pelo `TriggerRunner`.
|
|
690
|
+
- **`errors` e `logs` em `Trigger` são campos legados** — o método `SaveTrigger` que os populava está comentado. Nunca são preenchidos no fluxo atual.
|
|
691
|
+
- **Sem cancelamento via exceção** — `throw` dentro de um `before_create` script é capturado pelo `executor` (`exceptions.add`), logado, e a operação principal **continua mesmo assim**.
|
|
692
|
+
- **Sem broadcast de invalidação de cache em cluster** — cada nó decide por si baseado em `trigger.updated` vs. `dates`. Há janela de inconsistência: enquanto o nó B não recebe nenhum evento que dispare o trigger, ele continua com a classe antiga.
|
|
693
|
+
- **`@TimedInterrupt(value = 200L, ...)` é fixo no código** (linha 164 de `TriggerRunner.getScript`). Não há como configurar via `trigger.timeout` para passar de 200s.
|
|
694
|
+
- **`InterruptedException` no executor não chama `future.cancel(true)`** (linhas 308–314 de `TriggerRunner`) — possível leak de thread.
|
|
695
|
+
- **`TriggerManager.execute` engole TODA exception** — `catch(Exception)` com `e.printStackTrace()`, sem propagar nem registrar em log persistente. Bugs em managers chamadores são invisíveis.
|
|
696
|
+
- **Limite diário usa contador in-memory** — restart do serviço zera. Não há quota persistida.
|
|
697
|
+
- **Limite é shared entre todos os triggers do tenant** — não há quota por `trigger.id`. Quando o teto é atingido, **nenhum** trigger executa até o próximo reset de dia.
|
|
698
|
+
- **Sem audit log nativo** — operações em `/v3/trigger` não passam pelo `AuditManager` (diferente de `/v3/database`).
|
|
699
|
+
- **Sem filtro de tenant nas estatísticas detalhadas** quando comparado via `triggers` map — funciona porque `StatisticManager` é por `apiKey`, mas o `triggers` map cresce sem bound (todo `trigger.id` que executou pelo menos uma vez nunca é removido em memória).
|
|
700
|
+
- **`Trigger.entity` e `Trigger.event` aceitam qualquer string** — não há enum-enforcement. Triggers escritos para `event="after_win"` errado (ex: `"afterwin"`) **nunca disparam** e não há aviso.
|
|
701
|
+
- **`POST /v3/trigger/execute/{id}` ignora limite diário** — útil para debug, perigoso em prod.
|
|
702
|
+
- **Limit `dailyTriggerExecutionsLimit` é inclusivo** (`<=`) — quando o uso bate exatamente no limite ainda executa; bloqueia só do próximo em diante.
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## 8. Segurança e Permissões
|
|
707
|
+
|
|
708
|
+
### 8.1 Autenticação
|
|
709
|
+
|
|
710
|
+
- Todos os endpoints exigem Bearer token (`@BeanParam AuthBean authBean` → `FrontController.getInstance(authBean.getApiKey())`).
|
|
711
|
+
- **Não há filtro de role/permission no `TriggerRest`** — qualquer principal autenticado com `apiKey` válido cria/lê/deleta/executa triggers daquele tenant.
|
|
712
|
+
- Isolamento multi-tenant é via `apiKey` → `ManagerFactory` próprio → `Jongo` próprio → coleção `trigger` do banco daquele tenant.
|
|
713
|
+
|
|
714
|
+
### 8.2 Sandbox de scripts
|
|
715
|
+
|
|
716
|
+
- `SecureASTCustomizer` com `TriggerExpressionChecker.isAuthorized(...)`:
|
|
717
|
+
- Bloqueia tipos: `System`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject` (lista hard-coded em `TriggerExpressionChecker`).
|
|
718
|
+
- Bloqueia substrings em `.getText()`: `.execute`, `.getDB`, `.getMongo`, `.dropDatabase`.
|
|
719
|
+
- Whitelist de tokens (linhas 222–247 de `TriggerRunner.compile`): `=, +, -, *, /, %, **, ++, +=, --, ==, !=, <, <=, >, >=, ||, ||=, &&, &&=, [, ]`. **Note que `==` aparece duplicado** (linhas 234 e 238).
|
|
720
|
+
|
|
721
|
+
### 8.3 Brechas conhecidas no sandbox
|
|
722
|
+
|
|
723
|
+
- **`.execute` é bloqueado por busca de substring**, não por análise semântica. Strings em métodos como `executeAggregate`, `executeQuery` (ex: chamadas do `Jongo`) também são bloqueadas — falso positivo.
|
|
724
|
+
- O `getText()` do AST verifica a **representação textual da expressão**, não o tipo resolvido. Variáveis dinâmicas (`def x = "exec" + "ute"; obj[x]()`) podem contornar.
|
|
725
|
+
- O bloqueio é **AST-time** — só pega o que aparece no código fonte. Reflection via classes permitidas (ex: `ManagerFactory.class.getDeclaredMethods()`) é livre.
|
|
726
|
+
- Outras configurações de hardening do `SecureASTCustomizer` (`receiversBlackList`, `importsBlacklist`, `methodDefinitionAllowed`, `closuresAllowed`) **não estão configuradas**, então closures, definições de método e imports adicionais são permitidos.
|
|
727
|
+
- Já há imports automáticos a `ManagerFactory` e a todas as classes de domínio — scripts têm **acesso completo ao runtime do tenant**, incluindo Players, Achievements, Catalog, etc.
|
|
728
|
+
|
|
729
|
+
### 8.4 Injeção em queries
|
|
730
|
+
|
|
731
|
+
- `GET /v3/trigger?q=<raw>` injeta `<raw>` literalmente no `$match` do aggregate. O aggregate é fixado em `db.trigger.aggregate([...])` da coleção `trigger`, mas pode ser usado para **enumeração de scripts** (ex: regex em `script` para procurar credenciais hardcoded).
|
|
732
|
+
- `name` e `script` são `$regex` sem escape — possível DoS por regex catastrófico.
|
|
733
|
+
|
|
734
|
+
### 8.5 Outras superfícies
|
|
735
|
+
|
|
736
|
+
- `TriggerExpressionChecker` é instanciado a cada `compile` — não há cache do customizer (impacto desprezível).
|
|
737
|
+
- Scripts têm acesso ao `ManagerFactory`, então podem chamar `manager.getPlayerManager().findAll()` etc. Não há controle interno de what-can-a-script-do.
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## 9. Observabilidade e Troubleshooting
|
|
742
|
+
|
|
743
|
+
### 9.1 Como verificar se um trigger executou
|
|
744
|
+
|
|
745
|
+
```bash
|
|
746
|
+
# Buscar logs por trigger.id (item == trigger.id no fluxo atual)
|
|
747
|
+
GET /v3/database/trigger_log?q={"item":"<TRIGGER_ID>"}&orderby=time&reverse=true&max_results=20
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Cada log tem `out` (println), `err` (exceções) e `ms` (latência).
|
|
751
|
+
|
|
752
|
+
### 9.2 Trigger não dispara
|
|
753
|
+
|
|
754
|
+
Checklist (em ordem):
|
|
755
|
+
|
|
756
|
+
1. **`entity` e `event` batem com o ponto de chamada?** Conferir nos sites em `getTriggerManager().execute(<id>, <obj>, <entity>, <event>, <player>, <ctx>)`. Strings divergentes (`"action"` vs `"actions"`) silenciam.
|
|
757
|
+
2. **Limite diário não está estourado?**
|
|
758
|
+
```bash
|
|
759
|
+
GET /v3/limits
|
|
760
|
+
# Ver "trigger.max" e o atual consumido.
|
|
761
|
+
```
|
|
762
|
+
3. **`trigger.updated` é mais recente que o último `clear` no cluster?** Se você editou o trigger em um nó mas outro nó tem o cache antigo, dispare `PUT /v3/trigger/compile/all`.
|
|
763
|
+
4. **Compilação falhou?** O `POST /v3/trigger` retorna `status:"ERROR"` no body se sim. Reler o response. Observação: o fluxo do `POST /v3/trigger` é `compile → add`; se `compile` falha, `add` **não é chamado** — o script com erro **não** é persistido.
|
|
764
|
+
5. **`script` define `void trigger(event, entity, player, database)`?** Se não, `MissingMethodException` aparece em `trigger_log.err` na primeira execução.
|
|
765
|
+
|
|
766
|
+
### 9.3 Trigger demora demais / atinge timeout
|
|
767
|
+
|
|
768
|
+
- O log gravado terá `err: ["Timeout after Xs (timeout allowed Ys)", "<message>"]`.
|
|
769
|
+
- Se o script faz I/O remoto (`Unirest`, `HttpClientFunifier`), aumentar `trigger.timeout` até 200. Acima disso, o `@TimedInterrupt` mata mesmo assim.
|
|
770
|
+
|
|
771
|
+
### 9.4 Queries úteis
|
|
772
|
+
|
|
773
|
+
```js
|
|
774
|
+
// Triggers ativos em (entity, event)
|
|
775
|
+
db.trigger.find({ entity: "action", event: "after_win" })
|
|
776
|
+
|
|
777
|
+
// Triggers com timeout customizado
|
|
778
|
+
db.trigger.find({ timeout: { $exists: true, $gt: 0 } })
|
|
779
|
+
|
|
780
|
+
// Últimas execuções com erro
|
|
781
|
+
db.trigger_log.find({ err: { $ne: null } }).sort({ time: -1 }).limit(20)
|
|
782
|
+
|
|
783
|
+
// p95 de execução por trigger (aggregate)
|
|
784
|
+
db.trigger_log.aggregate([
|
|
785
|
+
{ $group: { _id: "$item", count: { $sum: 1 }, avg_ms: { $avg: "$ms" }, max_ms: { $max: "$ms" } } },
|
|
786
|
+
{ $sort: { avg_ms: -1 } }
|
|
787
|
+
])
|
|
788
|
+
|
|
789
|
+
// Tamanho do trigger_log (atenção: pode crescer indefinidamente)
|
|
790
|
+
db.trigger_log.count()
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### 9.5 Erros comuns
|
|
794
|
+
|
|
795
|
+
| Sintoma | Causa provável | Onde olhar |
|
|
796
|
+
|---|---|---|
|
|
797
|
+
| Script "não roda" mas POST OK | Limite diário estourado | `GET /v3/limits` |
|
|
798
|
+
| Funciona em dev, falha em prod | Cache local de outro nó | `PUT /v3/trigger/compile/all` |
|
|
799
|
+
| `MissingMethodException: trigger()` | Script sem a função `trigger(event, entity, player, database)` | `trigger_log.err` |
|
|
800
|
+
| `SecurityException` | AST bloqueou — `.execute`, `System`, ou token não-whitelisted | `trigger_log.err` |
|
|
801
|
+
| `TimeoutException` | Script passou de `trigger.timeout` ou de 200s (`@TimedInterrupt`) | `trigger_log.err` |
|
|
802
|
+
| Operação principal não foi cancelada apesar de exception no `before_*` | É by design — exceptions são engolidas | seção 5 |
|
|
803
|
+
| Coleção `trigger_log` cresceu muito | Não há TTL nativo no log | Criar índice TTL manualmente em `time` |
|
|
804
|
+
|
|
805
|
+
### 9.6 Diagnóstico via REST
|
|
806
|
+
|
|
807
|
+
```bash
|
|
808
|
+
# Detalhe atual de um trigger
|
|
809
|
+
GET /v3/trigger/<id>
|
|
810
|
+
|
|
811
|
+
# Listar últimos triggers criados
|
|
812
|
+
GET /v3/trigger?orderby=creation&reverse=true&max_results=10
|
|
813
|
+
|
|
814
|
+
# Stats de execução (in-memory por instância)
|
|
815
|
+
GET /v3/limits
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## 10. Exemplos Práticos
|
|
821
|
+
|
|
822
|
+
### 10.1 Mínimo funcional — log de evento
|
|
823
|
+
|
|
824
|
+
```json
|
|
825
|
+
POST /v3/trigger
|
|
826
|
+
{
|
|
827
|
+
"name": "Log after_win action",
|
|
828
|
+
"entity": "action",
|
|
829
|
+
"event": "after_win",
|
|
830
|
+
"script": "void trigger(event, entity, player, database) {\n println 'event=' + event + ' player=' + player + ' actionId=' + entity.getActionId()\n}"
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
Resultado esperado: cada `trackSynchonous(action)` deixa uma linha em `trigger_log.out`.
|
|
835
|
+
|
|
836
|
+
### 10.2 Avançado — atribuir Achievement extra de bônus de fim de semana
|
|
837
|
+
|
|
838
|
+
```json
|
|
839
|
+
POST /v3/trigger
|
|
840
|
+
{
|
|
841
|
+
"name": "Bônus de fim de semana",
|
|
842
|
+
"entity": "action",
|
|
843
|
+
"event": "before_win",
|
|
844
|
+
"timeout": 10,
|
|
845
|
+
"techniques": ["GT001"],
|
|
846
|
+
"references": [
|
|
847
|
+
{ "_id": "weekend_bonus", "type": "action", "title": "Sell weekend", "way": "from" }
|
|
848
|
+
],
|
|
849
|
+
"script": "void trigger(event, entity, player, database) {\n def cal = java.util.Calendar.getInstance()\n cal.setTime(entity.getTime())\n def dow = cal.get(java.util.Calendar.DAY_OF_WEEK)\n if (dow == java.util.Calendar.SATURDAY || dow == java.util.Calendar.SUNDAY) {\n def bonus = new com.funifier.engine.achievement.Achievement(\n com.funifier.engine.guid.Guid.newShortGuid(),\n player,\n 50.0,\n com.funifier.engine.achievement.Achievement.TYPE_POINT,\n 'xp',\n new java.util.Date()\n )\n // Empurra para a lista que o AchievementManager devolverá ao caller\n if (context != null && context.extra != null && context.extra['achievements'] != null) {\n context.extra['achievements'].add(bonus)\n }\n println 'weekend bonus +50 xp'\n }\n}"
|
|
850
|
+
}
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
Pontos a observar:
|
|
854
|
+
|
|
855
|
+
- Usa `context.extra['achievements']` (canal estabelecido por `ActionManager.trackSynchonous`).
|
|
856
|
+
- O Achievement é entregue ao caller via lista mutável no `TriggerContext.extra` — o `ActionManager` ao retornar adiciona ela ao resultado final.
|
|
857
|
+
- `techniques` e `references` ficam apenas como metadata.
|
|
858
|
+
|
|
859
|
+
### 10.3 Debug com `/execute`
|
|
860
|
+
|
|
861
|
+
```http
|
|
862
|
+
POST /v3/trigger/execute/<id>?player=test@user.com
|
|
863
|
+
Authorization: Bearer eyJ...
|
|
864
|
+
Content-Type: application/json
|
|
865
|
+
|
|
866
|
+
{ "actionId": "sell", "userId": "test@user.com", "attributes": { "value": 200 } }
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Resposta:
|
|
870
|
+
|
|
871
|
+
```json
|
|
872
|
+
{
|
|
873
|
+
"trigger": { ... },
|
|
874
|
+
"exceptions": [],
|
|
875
|
+
"outputs": ["weekend bonus +50 xp"],
|
|
876
|
+
"millis": 19
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
Observação: este endpoint **não respeita `dailyTriggerExecutionsLimit`** e passa `context = null`. Scripts que dependem de `context.extra` lançarão NPE — testar isso é parte do debug.
|
|
881
|
+
|
|
882
|
+
### 10.4 Anti-pattern — exception como cancelamento
|
|
883
|
+
|
|
884
|
+
❌ **Não fazer:**
|
|
885
|
+
|
|
886
|
+
```groovy
|
|
887
|
+
// Tentativa de impedir o save de um action_log
|
|
888
|
+
void trigger(event, entity, player, database) {
|
|
889
|
+
if (entity.getAttributes() == null) {
|
|
890
|
+
throw new RuntimeException("attributes obrigatórios")
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Por quê: `TriggerManager.execute` faz `catch(Exception)` com `e.printStackTrace()` e segue. A action é gravada de qualquer jeito; o erro vai para o stdout do servidor (e em `trigger_log.err`), mas o caller não é notificado.
|
|
896
|
+
|
|
897
|
+
✅ **Em vez disso:** se você precisa veto, mover a validação para os checks no próprio `ActionManager` ou usar um `before_purchase_validation`/`before_acquire_validation` que o caller específico realmente respeite (não os `before_create`/`before_win` genéricos).
|
|
898
|
+
|
|
899
|
+
### 10.5 Anti-pattern — recompilação custosa em loop
|
|
900
|
+
|
|
901
|
+
❌ **Não fazer:**
|
|
902
|
+
|
|
903
|
+
```groovy
|
|
904
|
+
void trigger(event, entity, player, database) {
|
|
905
|
+
// Atualiza este próprio trigger a cada execução
|
|
906
|
+
def t = database.getCollection('trigger').findOne('{_id:#}', '<this trigger id>').as(com.funifier.engine.integration.trigger.Trigger.class)
|
|
907
|
+
t.updated = new Date()
|
|
908
|
+
manager.getTriggerManager().add(t)
|
|
909
|
+
}
|
|
910
|
+
```
|
|
185
911
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
912
|
+
Por quê: alterar `updated` invalida o cache local. Próxima execução fará `compile` (~10–50ms). Em alta-volume, isso vira o gargalo.
|
|
913
|
+
|
|
914
|
+
---
|
|
915
|
+
|
|
916
|
+
## Checklist de Configuração
|
|
917
|
+
|
|
918
|
+
- [ ] `entity` e `event` correspondem a uma combinação **realmente disparada** por algum manager — consultar seção 1 (lista de chamadores) e seção 3.1 (eventos extras ad-hoc).
|
|
919
|
+
- [ ] `script` define `void trigger(event, entity, player, database) { ... }`.
|
|
920
|
+
- [ ] `script` **não** usa `System.*`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject`, nem chama `.execute`/`.getDB`/`.getMongo`/`.dropDatabase` — senão `SecurityException` no AST.
|
|
921
|
+
- [ ] Se o script faz I/O remoto (HTTP, e-mail), setar `timeout` explícito (até **200 segundos** — acima disso o `@TimedInterrupt` mata mesmo).
|
|
922
|
+
- [ ] `GamificationLimits.dailyTriggerExecutions` do tenant comporta o volume esperado — limite zero bloqueia tudo silenciosamente.
|
|
923
|
+
- [ ] Após criar/atualizar via API em ambiente cluster: chamar `PUT /v3/trigger/compile/all` em cada nó (ou aguardar que `TriggerRunner` recompile sozinho via comparação `dates < updated`).
|
|
924
|
+
- [ ] **Armadilha (silencioso):** se você usar `entity:"action_log"` ou `entity:"achievement"`, **nenhum trigger dispara** — essas entidades foram comentadas em `Trigger.java` e os managers usam `Trigger.ENTITY_ACTION` (`"action"`) para o evento de gravação do `ActionLog`, e disparam por `Entity.POINT_CATEGORY.collection` / `Entity.CHALLENGE.collection` para Achievements (não pelo nome `"achievement"`).
|
|
925
|
+
- [ ] **Armadilha (legacy):** `Trigger.errors` e `Trigger.logs` no schema **não são populados** no runtime atual — consultar `trigger_log` em vez disso.
|
|
926
|
+
- [ ] **Armadilha (timeout duplo):** se um script demora mais de 200s independentemente do `timeout` configurado, é o `@TimedInterrupt` em `TriggerRunner.getScript` — não há configuração externa para mudar isso.
|
|
927
|
+
- [ ] **Armadilha (cancelamento):** lançar exceção em `before_create`/`before_win` **não cancela** a operação principal — o caller engole e segue.
|
|
928
|
+
- [ ] **Armadilha (debug ≠ produção):** `POST /v3/trigger/execute/{id}` passa `context = null` e ignora o limite diário. Scripts validados nesse endpoint podem quebrar em prod por NPE em `context.extra` ou por hit no limite.
|
|
929
|
+
- [ ] **Armadilha (sem 404):** `GET /v3/trigger/{id}` de um id inexistente retorna `null` com HTTP 200, não 404.
|
|
930
|
+
- [ ] **Armadilha (HTTP 201 com erro):** `POST /v3/trigger` retorna `201 Created` mesmo quando `status: "ERROR"` no body — sempre checar `status` antes de assumir sucesso.
|
|
931
|
+
- [ ] `Trigger.techniques`, `Trigger.style`, `Trigger.references` são apenas Studio metadata — não usar para lógica de runtime.
|