|
7 | 7 | recordDecision, |
8 | 8 | type DecisionOutcome, |
9 | 9 | type DecisionReason, |
| 10 | + type RecordDecisionOptions, |
10 | 11 | } from "./mollifierTelemetry.server"; |
11 | 12 |
|
12 | 13 | // `count` is the fleet-wide fixed-window counter for the env (INCR with a |
@@ -80,7 +81,7 @@ export type GateDependencies = { |
80 | 81 | inputs: GateInputs, |
81 | 82 | decision: Extract<TripDecision, { divert: true }>, |
82 | 83 | ) => void; |
83 | | - recordDecision: (outcome: DecisionOutcome, reason?: DecisionReason) => void; |
| 84 | + recordDecision: (outcome: DecisionOutcome, opts: RecordDecisionOptions) => void; |
84 | 85 | }; |
85 | 86 |
|
86 | 87 | // `options` is a thunk so env reads happen per-evaluation, not at module load. |
@@ -152,52 +153,59 @@ export async function evaluateGate( |
152 | 153 | ): Promise<GateOutcome> { |
153 | 154 | const d = { ...defaultGateDependencies, ...deps }; |
154 | 155 |
|
| 156 | + // Resolve the per-org flag up front so every decision below — including |
| 157 | + // the bypasses — can be labelled enrolled vs not on the |
| 158 | + // `mollifier.decisions` counter. Fail open: a transient error must not |
| 159 | + // block triggers. The resolver is purely in-memory (reads |
| 160 | + // `Organization.featureFlags`); it adds no DB round-trip to the hot path. |
| 161 | + let orgFlagEnabled: boolean; |
| 162 | + try { |
| 163 | + orgFlagEnabled = await d.resolveOrgFlag(inputs); |
| 164 | + } catch (error) { |
| 165 | + logger.warn("mollifier.resolve_org_flag_failed", { |
| 166 | + envId: inputs.envId, |
| 167 | + orgId: inputs.orgId, |
| 168 | + taskId: inputs.taskId, |
| 169 | + error: error instanceof Error ? error.message : String(error), |
| 170 | + }); |
| 171 | + orgFlagEnabled = false; |
| 172 | + } |
| 173 | + // Passed to every `recordDecision`. `org` only becomes a label for the |
| 174 | + // (operationally capped) enrolled cohort — the guard is in |
| 175 | + // `decisionLabels`, so passing orgId unconditionally here is safe. |
| 176 | + const labels: RecordDecisionOptions = { enrolled: orgFlagEnabled, orgId: inputs.orgId }; |
| 177 | + |
155 | 178 | // Debounce bypass. onDebounced is a closure over webapp state and |
156 | 179 | // can't be snapshotted into the buffer for drainer replay. Skip before the |
157 | 180 | // trip evaluator so debounce traffic is never counted against the rate. |
158 | 181 | if (inputs.options?.debounce) { |
159 | | - d.recordDecision("pass_through"); |
| 182 | + d.recordDecision("pass_through", labels); |
160 | 183 | return { action: "pass_through" }; |
161 | 184 | } |
162 | 185 | // OneTimeUseToken bypass. OTU is a security feature on the PUBLIC_JWT |
163 | 186 | // auth path; its synchronous-rejection contract is materially worse to |
164 | 187 | // break than the idempotency-key contract. |
165 | 188 | if (inputs.options?.oneTimeUseToken) { |
166 | | - d.recordDecision("pass_through"); |
| 189 | + d.recordDecision("pass_through", labels); |
167 | 190 | return { action: "pass_through" }; |
168 | 191 | } |
169 | 192 | // Single triggerAndWait bypass. batchTriggerAndWait still funnels |
170 | 193 | // through TriggerTaskService.call per item so the dominant burst pattern |
171 | 194 | // remains covered. |
172 | 195 | if (inputs.options?.parentTaskRunId && inputs.options?.resumeParentOnCompletion) { |
173 | | - d.recordDecision("pass_through"); |
| 196 | + d.recordDecision("pass_through", labels); |
174 | 197 | return { action: "pass_through" }; |
175 | 198 | } |
176 | 199 |
|
177 | 200 | if (!d.isMollifierEnabled()) { |
178 | | - d.recordDecision("pass_through"); |
| 201 | + d.recordDecision("pass_through", labels); |
179 | 202 | return { action: "pass_through" }; |
180 | 203 | } |
181 | 204 |
|
182 | | - // Fail open: a transient DB error resolving the per-org flag must not |
183 | | - // block triggers. Mirror the evaluator's fail-open posture in |
184 | | - // `mollifierTripEvaluator.server.ts`. |
185 | | - let orgFlagEnabled: boolean; |
186 | | - try { |
187 | | - orgFlagEnabled = await d.resolveOrgFlag(inputs); |
188 | | - } catch (error) { |
189 | | - logger.warn("mollifier.resolve_org_flag_failed", { |
190 | | - envId: inputs.envId, |
191 | | - orgId: inputs.orgId, |
192 | | - taskId: inputs.taskId, |
193 | | - error: error instanceof Error ? error.message : String(error), |
194 | | - }); |
195 | | - orgFlagEnabled = false; |
196 | | - } |
197 | 205 | const shadowOn = d.isShadowModeOn(); |
198 | 206 |
|
199 | 207 | if (!orgFlagEnabled && !shadowOn) { |
200 | | - d.recordDecision("pass_through"); |
| 208 | + d.recordDecision("pass_through", labels); |
201 | 209 | return { action: "pass_through" }; |
202 | 210 | } |
203 | 211 |
|
@@ -226,17 +234,17 @@ export async function evaluateGate( |
226 | 234 | decision = { divert: false }; |
227 | 235 | } |
228 | 236 | if (!decision.divert) { |
229 | | - d.recordDecision("pass_through"); |
| 237 | + d.recordDecision("pass_through", labels); |
230 | 238 | return { action: "pass_through" }; |
231 | 239 | } |
232 | 240 |
|
233 | 241 | if (orgFlagEnabled) { |
234 | 242 | d.logMollified(inputs, decision); |
235 | | - d.recordDecision("mollify", decision.reason); |
| 243 | + d.recordDecision("mollify", { ...labels, reason: decision.reason }); |
236 | 244 | return { action: "mollify", decision }; |
237 | 245 | } |
238 | 246 |
|
239 | 247 | d.logShadow(inputs, decision); |
240 | | - d.recordDecision("shadow_log", decision.reason); |
| 248 | + d.recordDecision("shadow_log", { ...labels, reason: decision.reason }); |
241 | 249 | return { action: "shadow_log", decision }; |
242 | 250 | } |
0 commit comments