Skip to content

Commit d366bd3

Browse files
author
stlc-bot
committed
feat(webhooks): add configurable retries and manual replay
Stainless-Generated-From: e8e908c91e4d4729adff54498dab7cd889708218
1 parent 34071f9 commit d366bd3

8 files changed

Lines changed: 509 additions & 8 deletions

File tree

.stats.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
configured_endpoints: 41
1+
configured_endpoints: 45

pkg/cmd/batch.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ var batchGetResults = cli.Command{
128128
HideHelpCommand: true,
129129
}
130130

131-
var batchSubmit = cli.Command{
131+
var batchSubmit = requestflag.WithInnerFlags(cli.Command{
132132
Name: "submit",
133133
Usage: "Scrape 25K URLs or crawl large websites asynchronously.",
134134
Suggest: true,
@@ -144,9 +144,14 @@ var batchSubmit = cli.Command{
144144
Usage: "Tags stored on the batch. Filter the batch list by them later.",
145145
BodyPath: "tags",
146146
},
147+
&requestflag.Flag[map[string]any]{
148+
Name: "webhook",
149+
Usage: "Completion webhook settings. Cannot be combined with webhookUrl. Omitting retry preserves legacy delivery; retry: {} opts into durable retries.",
150+
BodyPath: "webhook",
151+
},
147152
&requestflag.Flag[string]{
148153
Name: "webhook-url",
149-
Usage: "URL notified when the batch finishes.",
154+
Usage: "Legacy URL notified when the batch finishes. Preserves one best-effort attempt. Cannot be combined with webhook.",
150155
BodyPath: "webhookUrl",
151156
},
152157
&requestflag.Flag[string]{
@@ -157,7 +162,19 @@ var batchSubmit = cli.Command{
157162
},
158163
Action: handleBatchSubmit,
159164
HideHelpCommand: true,
160-
}
165+
}, map[string][]requestflag.HasOuterFlag{
166+
"webhook": {
167+
&requestflag.InnerFlag[string]{
168+
Name: "webhook.url",
169+
InnerField: "url",
170+
},
171+
&requestflag.InnerFlag[map[string]any]{
172+
Name: "webhook.retry",
173+
Usage: "Opt into durable webhook delivery. An empty object uses the default retry schedule. Omit retry to preserve legacy delivery behavior. The policy is snapshotted for each event.",
174+
InnerField: "retry",
175+
},
176+
},
177+
})
161178

162179
func handleBatchRetrieve(ctx context.Context, cmd *cli.Command) error {
163180
client := contextdev.NewClient(getDefaultRequestOptions(cmd)...)

pkg/cmd/batch_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/context-dot-dev/context-dev-cli/internal/mocktest"
9+
"github.com/context-dot-dev/context-dev-cli/internal/requestflag"
910
)
1011

1112
func TestBatchRetrieve(t *testing.T) {
@@ -85,6 +86,26 @@ func TestBatchSubmit(t *testing.T) {
8586
"--input", "{data: {format: markdown, urls: [{url: https://example.com/products/anvil, itemId: sku-1, meta: {category: bar}}, {url: https://example.com/products/hammer, itemId: sku-2, meta: {foo: bar}}], options: {country: de, excludeSelectors: [x], includeHTML: true, includeImages: true, includeLinks: true, includeSelectors: [x], maxAgeMs: 0, pdf: {end: 1, ocr: true, shouldParse: true, start: 1}, settleAnimations: true, shortenBase64Images: true, useMainContentOnly: true, waitForMs: 0}}, mode: scrape}",
8687
"--tag", "docs",
8788
"--tag", "competitor",
89+
"--webhook", "{url: https://example.com, retry: {delays_seconds: [10, 60, 300, 1800, 7200, 21600, 57600]}}",
90+
"--webhook-url", "webhookUrl",
91+
"--idempotency-key", "Idempotency-Key",
92+
)
93+
})
94+
95+
t.Run("inner flags", func(t *testing.T) {
96+
// Check that inner flags have been set up correctly
97+
requestflag.CheckInnerFlags(batchSubmit)
98+
99+
// Alternative argument passing style using inner flags
100+
mocktest.TestRunMockTestWithFlags(
101+
t,
102+
"--api-key", "string",
103+
"batch", "submit",
104+
"--input", "{data: {format: markdown, urls: [{url: https://example.com/products/anvil, itemId: sku-1, meta: {category: bar}}, {url: https://example.com/products/hammer, itemId: sku-2, meta: {foo: bar}}], options: {country: de, excludeSelectors: [x], includeHTML: true, includeImages: true, includeLinks: true, includeSelectors: [x], maxAgeMs: 0, pdf: {end: 1, ocr: true, shouldParse: true, start: 1}, settleAnimations: true, shortenBase64Images: true, useMainContentOnly: true, waitForMs: 0}}, mode: scrape}",
105+
"--tag", "docs",
106+
"--tag", "competitor",
107+
"--webhook.url", "https://example.com",
108+
"--webhook.retry", "{delays_seconds: [10, 60, 300, 1800, 7200, 21600, 57600]}",
88109
"--webhook-url", "webhookUrl",
89110
"--idempotency-key", "Idempotency-Key",
90111
)
@@ -128,6 +149,17 @@ func TestBatchSubmit(t *testing.T) {
128149
"tags:\n" +
129150
" - docs\n" +
130151
" - competitor\n" +
152+
"webhook:\n" +
153+
" url: https://example.com\n" +
154+
" retry:\n" +
155+
" delays_seconds:\n" +
156+
" - 10\n" +
157+
" - 60\n" +
158+
" - 300\n" +
159+
" - 1800\n" +
160+
" - 7200\n" +
161+
" - 21600\n" +
162+
" - 57600\n" +
131163
"webhookUrl: webhookUrl\n")
132164
mocktest.TestRunMockTestWithPipeAndFlags(
133165
t, pipeData,

pkg/cmd/cmd.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,17 @@ func init() {
179179
&batchSubmit,
180180
},
181181
},
182+
{
183+
Name: "webhooks:deliveries",
184+
Category: "API RESOURCE",
185+
Suggest: true,
186+
Commands: []*cli.Command{
187+
&webhooksDeliveriesRetrieve,
188+
&webhooksDeliveriesList,
189+
&webhooksDeliveriesListAttempts,
190+
&webhooksDeliveriesRetry,
191+
},
192+
},
182193
{
183194
Name: "people",
184195
Category: "API RESOURCE",

pkg/cmd/monitor.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ var monitorsCreate = requestflag.WithInnerFlags(cli.Command{
8686
Usage: "Events delivered to this endpoint. `change.detected` fires only when a run detects a change; `run.completed` fires on every completed run — including runs that detected no change — and embeds the change when one was detected. Defaults to `[\"change.detected\"]` when omitted.",
8787
InnerField: "events",
8888
},
89+
&requestflag.InnerFlag[map[string]any]{
90+
Name: "webhook.retry",
91+
Usage: "Opt into durable webhook delivery. An empty object uses the default retry schedule. Omit retry to preserve legacy delivery behavior. The policy is snapshotted for each event.",
92+
InnerField: "retry",
93+
},
8994
&requestflag.InnerFlag[string]{
9095
Name: "webhook.secret",
9196
Usage: "Signing secret used to verify webhook authenticity. Each delivery includes an `X-Context-Signature: t=<unix>,v1=<hmac>` header, where the HMAC is SHA-256 over `\"{t}.{rawRequestBody}\"` keyed by this secret. Recompute it with a constant-time compare and reject stale timestamps to prevent replay. Generated by the API; cannot be set by clients.",
@@ -185,6 +190,11 @@ var monitorsUpdate = requestflag.WithInnerFlags(cli.Command{
185190
Usage: "Events delivered to this endpoint. `change.detected` fires only when a run detects a change; `run.completed` fires on every completed run — including runs that detected no change — and embeds the change when one was detected. Defaults to `[\"change.detected\"]` when omitted.",
186191
InnerField: "events",
187192
},
193+
&requestflag.InnerFlag[map[string]any]{
194+
Name: "webhook.retry",
195+
Usage: "Opt into durable webhook delivery. An empty object uses the default retry schedule. Omit retry to preserve legacy delivery behavior. The policy is snapshotted for each event.",
196+
InnerField: "retry",
197+
},
188198
&requestflag.InnerFlag[string]{
189199
Name: "webhook.secret",
190200
Usage: "Signing secret used to verify webhook authenticity. Each delivery includes an `X-Context-Signature: t=<unix>,v1=<hmac>` header, where the HMAC is SHA-256 over `\"{t}.{rawRequestBody}\"` keyed by this secret. Recompute it with a constant-time compare and reject stale timestamps to prevent replay. Generated by the API; cannot be set by clients.",

pkg/cmd/monitor_test.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func TestMonitorsCreate(t *testing.T) {
2323
"--schedule", "{frequency: 6, type: interval, unit: hours}",
2424
"--tag", "pricing",
2525
"--tag", "competitor",
26-
"--webhook", "{url: https://example.com/webhook, events: [change.detected, run.completed]}",
26+
"--webhook", "{url: https://example.com/webhook, events: [change.detected, run.completed], retry: {delays_seconds: [10, 60, 300, 1800, 7200, 21600, 57600]}}",
2727
)
2828
})
2929

@@ -47,6 +47,7 @@ func TestMonitorsCreate(t *testing.T) {
4747
"--tag", "competitor",
4848
"--webhook.url", "https://example.com/webhook",
4949
"--webhook.events", "[change.detected, run.completed]",
50+
"--webhook.retry", "{delays_seconds: [10, 60, 300, 1800, 7200, 21600, 57600]}",
5051
)
5152
})
5253

@@ -75,7 +76,16 @@ func TestMonitorsCreate(t *testing.T) {
7576
" url: https://example.com/webhook\n" +
7677
" events:\n" +
7778
" - change.detected\n" +
78-
" - run.completed\n")
79+
" - run.completed\n" +
80+
" retry:\n" +
81+
" delays_seconds:\n" +
82+
" - 10\n" +
83+
" - 60\n" +
84+
" - 300\n" +
85+
" - 1800\n" +
86+
" - 7200\n" +
87+
" - 21600\n" +
88+
" - 57600\n")
7989
mocktest.TestRunMockTestWithPipeAndFlags(
8090
t, pipeData,
8191
"--api-key", "string",
@@ -111,7 +121,7 @@ func TestMonitorsUpdate(t *testing.T) {
111121
"--tag", "pricing",
112122
"--tag", "competitor",
113123
"--target", "{type: page, url: https://acme.com/pricing, instructions: 'Report pricing or plan availability changes. Ignore counters, timestamps, testimonials, and navigation.', normalize_whitespace: true}",
114-
"--webhook", "{url: https://example.com/webhook, events: [change.detected, run.completed]}",
124+
"--webhook", "{url: https://example.com/webhook, events: [change.detected, run.completed], retry: {delays_seconds: [10, 60, 300, 1800, 7200, 21600, 57600]}}",
115125
)
116126
})
117127

@@ -136,6 +146,7 @@ func TestMonitorsUpdate(t *testing.T) {
136146
"--target", "{type: page, url: https://acme.com/pricing, instructions: 'Report pricing or plan availability changes. Ignore counters, timestamps, testimonials, and navigation.', normalize_whitespace: true}",
137147
"--webhook.url", "https://example.com/webhook",
138148
"--webhook.events", "[change.detected, run.completed]",
149+
"--webhook.retry", "{delays_seconds: [10, 60, 300, 1800, 7200, 21600, 57600]}",
139150
)
140151
})
141152

@@ -164,7 +175,16 @@ func TestMonitorsUpdate(t *testing.T) {
164175
" url: https://example.com/webhook\n" +
165176
" events:\n" +
166177
" - change.detected\n" +
167-
" - run.completed\n")
178+
" - run.completed\n" +
179+
" retry:\n" +
180+
" delays_seconds:\n" +
181+
" - 10\n" +
182+
" - 60\n" +
183+
" - 300\n" +
184+
" - 1800\n" +
185+
" - 7200\n" +
186+
" - 21600\n" +
187+
" - 57600\n")
168188
mocktest.TestRunMockTestWithPipeAndFlags(
169189
t, pipeData,
170190
"--api-key", "string",

0 commit comments

Comments
 (0)