Skip to content

Commit acf6941

Browse files
committed
fix(data-drains): correct Datadog size guard and Snowflake VARIANT limit
- Datadog payload guard now checks the uncompressed size against the 5 MB limit and the wire size against the 6 MB compressed limit, so gzip cannot smuggle an oversized body past the client-side check. - Snowflake VARIANT limit is 16 MiB (16,777,216 bytes), not 16,000,000 bytes — small payloads between 16 MB and 16 MiB were being rejected unnecessarily. - Drop the unused apiKey field on Datadog PostInput; the key is already embedded in the prepared request headers.
1 parent fde60c1 commit acf6941

3 files changed

Lines changed: 35 additions & 17 deletions

File tree

apps/sim/lib/data-drains/destinations/datadog.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@ const MAX_ATTEMPTS = 4
3030
const BASE_BACKOFF_MS = 500
3131
const MAX_BACKOFF_MS = 30_000
3232
const PER_ATTEMPT_TIMEOUT_MS = 30_000
33-
/** Datadog v2 logs intake limits: 5 MB per request, 1 MB per entry, 1000 entries per request. */
34-
const MAX_REQUEST_BYTES = 5 * 1024 * 1024
33+
/**
34+
* Datadog v2 logs intake limits: 5 MB uncompressed per request, 6 MB compressed
35+
* (we enforce both since gzip can hide a too-large body), 1 MB per entry, 1000
36+
* entries per request. https://docs.datadoghq.com/api/latest/logs/
37+
*/
38+
const MAX_UNCOMPRESSED_BYTES = 5 * 1024 * 1024
39+
const MAX_COMPRESSED_BYTES = 6 * 1024 * 1024
3540
const MAX_ENTRY_BYTES = 1024 * 1024
3641
const MAX_ENTRIES_PER_REQUEST = 1000
3742
/** Compress payloads above this threshold; gzip overhead isn't worth it on small bodies. */
@@ -127,21 +132,23 @@ function backoffWithJitter(attempt: number, retryAfterMs: number | null): number
127132
interface PreparedBody {
128133
body: Uint8Array | string
129134
headers: Record<string, string>
130-
bytes: number
135+
/** On-the-wire (post-gzip) size — what Datadog measures against its compressed limit. */
136+
wireBytes: number
137+
/** Uncompressed payload size — what Datadog measures against its uncompressed limit. */
138+
rawBytes: number
131139
}
132140

133141
interface PostInput {
134142
url: string
135-
apiKey: string
136143
prepared: PreparedBody
137144
signal: AbortSignal
138145
}
139146

140147
/**
141148
* Builds the request body and headers, applying gzip compression for payloads
142149
* above {@link GZIP_THRESHOLD_BYTES}. Returns the wire body (Buffer or string)
143-
* along with the headers describing it. The {@link MAX_REQUEST_BYTES} guard
144-
* applies to whatever is returned here (post-compression when applicable).
150+
* along with the headers describing it. Both raw and wire sizes are returned
151+
* so callers can enforce Datadog's 5 MB uncompressed / 6 MB compressed limits.
145152
*/
146153
function buildRequestBody(payload: string, apiKey: string): PreparedBody {
147154
const headers: Record<string, string> = {
@@ -156,9 +163,9 @@ function buildRequestBody(payload: string, apiKey: string): PreparedBody {
156163
headers['Content-Encoding'] = 'gzip'
157164
// Re-wrap as a plain Uint8Array view so the fetch BodyInit overload matches.
158165
const view = new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.byteLength)
159-
return { body: view, headers, bytes: view.byteLength }
166+
return { body: view, headers, wireBytes: view.byteLength, rawBytes }
160167
}
161-
return { body: payload, headers, bytes: rawBytes }
168+
return { body: payload, headers, wireBytes: rawBytes, rawBytes }
162169
}
163170

164171
async function postWithRetries(input: PostInput): Promise<Response> {
@@ -217,7 +224,6 @@ export const datadogDestination: DrainDestination<
217224
]
218225
await postWithRetries({
219226
url: buildEndpoint(config.site),
220-
apiKey: credentials.apiKey,
221227
prepared: buildRequestBody(JSON.stringify(probe), credentials.apiKey),
222228
signal,
223229
})
@@ -244,22 +250,29 @@ export const datadogDestination: DrainDestination<
244250
}
245251
const payload = JSON.stringify(entries)
246252
const prepared = buildRequestBody(payload, credentials.apiKey)
247-
if (prepared.bytes > MAX_REQUEST_BYTES) {
253+
// Reject before sending so we surface a clean client-side error instead
254+
// of letting Datadog return a confusing HTTP 413 after decompression.
255+
if (prepared.rawBytes > MAX_UNCOMPRESSED_BYTES) {
256+
throw new Error(
257+
`Datadog payload is ${prepared.rawBytes} bytes uncompressed, exceeds the ${MAX_UNCOMPRESSED_BYTES}-byte per-request limit`
258+
)
259+
}
260+
if (prepared.wireBytes > MAX_COMPRESSED_BYTES) {
248261
throw new Error(
249-
`Datadog payload is ${prepared.bytes} bytes, exceeds the ${MAX_REQUEST_BYTES}-byte per-request limit`
262+
`Datadog payload is ${prepared.wireBytes} bytes on the wire, exceeds the ${MAX_COMPRESSED_BYTES}-byte compressed per-request limit`
250263
)
251264
}
252265
const response = await postWithRetries({
253266
url,
254-
apiKey: credentials.apiKey,
255267
prepared,
256268
signal,
257269
})
258270
const requestId = response.headers.get('dd-request-id') ?? null
259271
logger.debug('Datadog chunk delivered', {
260272
site: config.site,
261273
rows: entries.length,
262-
bytes: prepared.bytes,
274+
rawBytes: prepared.rawBytes,
275+
wireBytes: prepared.wireBytes,
263276
})
264277
return {
265278
locator: requestId

apps/sim/lib/data-drains/destinations/snowflake.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ describe('snowflakeDestination', () => {
179179
await session.close()
180180
})
181181

182-
it('throws a clear error when a binding exceeds the 16 MB VARIANT limit', async () => {
182+
it('throws a clear error when a binding exceeds the 16 MiB VARIANT limit', async () => {
183183
const session = snowflakeDestination.openSession({ config, credentials })
184-
const huge = `"${'a'.repeat(16_000_001)}"`
184+
const huge = `"${'a'.repeat(16 * 1024 * 1024 + 1)}"`
185185
const body = Buffer.from(`${huge}\n`, 'utf8')
186186
await expect(
187187
session.deliver({

apps/sim/lib/data-drains/destinations/snowflake.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ const POLL_DEADLINE_MS = 10 * 60_000
2222
const EXECUTE_MAX_ATTEMPTS = 3
2323
const EXECUTE_RETRY_BASE_DELAY_MS = 500
2424
const EXECUTE_RETRY_MAX_DELAY_MS = 5_000
25-
/** Snowflake VARIANT max value size is 16 MB. */
26-
const VARIANT_MAX_BYTES = 16_000_000
25+
/**
26+
* Snowflake VARIANT max value size is 16 MiB (16,777,216 bytes) on accounts
27+
* before the 2025_03 behavior change bundle, and 128 MB after it. We use the
28+
* conservative pre-bundle limit so the same value works on every account.
29+
* https://docs.snowflake.com/en/release-notes/bcr-bundles/2025_03/bcr-1942
30+
*/
31+
const VARIANT_MAX_BYTES = 16 * 1024 * 1024
2732

2833
/**
2934
* Snowflake JWT `iss`/`sub` require the bare account identifier without any

0 commit comments

Comments
 (0)