monday_ruby 1.1.0 → 1.2.0
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.
- checksums.yaml +4 -4
- data/.env +1 -1
- data/.rubocop.yml +2 -1
- data/CHANGELOG.md +14 -0
- data/CONTRIBUTING.md +104 -0
- data/README.md +146 -142
- data/docs/.vitepress/config.mjs +255 -0
- data/docs/.vitepress/theme/index.js +4 -0
- data/docs/.vitepress/theme/style.css +43 -0
- data/docs/README.md +80 -0
- data/docs/explanation/architecture.md +507 -0
- data/docs/explanation/best-practices/errors.md +478 -0
- data/docs/explanation/best-practices/performance.md +1084 -0
- data/docs/explanation/best-practices/rate-limiting.md +630 -0
- data/docs/explanation/best-practices/testing.md +820 -0
- data/docs/explanation/column-values.md +857 -0
- data/docs/explanation/design.md +795 -0
- data/docs/explanation/graphql.md +356 -0
- data/docs/explanation/migration/v1.md +808 -0
- data/docs/explanation/pagination.md +447 -0
- data/docs/guides/advanced/batch.md +1274 -0
- data/docs/guides/advanced/complex-queries.md +1114 -0
- data/docs/guides/advanced/errors.md +818 -0
- data/docs/guides/advanced/pagination.md +934 -0
- data/docs/guides/advanced/rate-limiting.md +981 -0
- data/docs/guides/authentication.md +286 -0
- data/docs/guides/boards/create.md +386 -0
- data/docs/guides/boards/delete.md +405 -0
- data/docs/guides/boards/duplicate.md +511 -0
- data/docs/guides/boards/query.md +530 -0
- data/docs/guides/boards/update.md +453 -0
- data/docs/guides/columns/create.md +452 -0
- data/docs/guides/columns/metadata.md +492 -0
- data/docs/guides/columns/query.md +455 -0
- data/docs/guides/columns/update-multiple.md +459 -0
- data/docs/guides/columns/update-values.md +509 -0
- data/docs/guides/files/add-to-column.md +40 -0
- data/docs/guides/files/add-to-update.md +37 -0
- data/docs/guides/files/clear-column.md +33 -0
- data/docs/guides/first-request.md +285 -0
- data/docs/guides/folders/manage.md +750 -0
- data/docs/guides/groups/items.md +626 -0
- data/docs/guides/groups/manage.md +501 -0
- data/docs/guides/installation.md +169 -0
- data/docs/guides/items/create.md +493 -0
- data/docs/guides/items/delete.md +514 -0
- data/docs/guides/items/query.md +605 -0
- data/docs/guides/items/subitems.md +483 -0
- data/docs/guides/items/update.md +699 -0
- data/docs/guides/updates/manage.md +619 -0
- data/docs/guides/use-cases/dashboard.md +1421 -0
- data/docs/guides/use-cases/import.md +1962 -0
- data/docs/guides/use-cases/task-management.md +1381 -0
- data/docs/guides/workspaces/manage.md +502 -0
- data/docs/index.md +69 -0
- data/docs/package-lock.json +2468 -0
- data/docs/package.json +13 -0
- data/docs/reference/client.md +540 -0
- data/docs/reference/configuration.md +586 -0
- data/docs/reference/errors.md +693 -0
- data/docs/reference/resources/account.md +208 -0
- data/docs/reference/resources/activity-log.md +369 -0
- data/docs/reference/resources/board-view.md +359 -0
- data/docs/reference/resources/board.md +393 -0
- data/docs/reference/resources/column.md +543 -0
- data/docs/reference/resources/file.md +236 -0
- data/docs/reference/resources/folder.md +386 -0
- data/docs/reference/resources/group.md +507 -0
- data/docs/reference/resources/item.md +348 -0
- data/docs/reference/resources/subitem.md +267 -0
- data/docs/reference/resources/update.md +259 -0
- data/docs/reference/resources/workspace.md +213 -0
- data/docs/reference/response.md +560 -0
- data/docs/tutorial/first-integration.md +713 -0
- data/lib/monday/client.rb +24 -0
- data/lib/monday/configuration.rb +5 -0
- data/lib/monday/request.rb +15 -0
- data/lib/monday/resources/base.rb +4 -0
- data/lib/monday/resources/file.rb +56 -0
- data/lib/monday/util.rb +1 -0
- data/lib/monday/version.rb +1 -1
- metadata +87 -4
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
# Handle Rate Limits
|
|
2
|
+
|
|
3
|
+
Manage monday.com API rate limits and complexity budgets to prevent errors and optimize API usage.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [Installed and configured](/guides/installation) monday_ruby
|
|
8
|
+
- [Set up authentication](/guides/authentication) with your API token
|
|
9
|
+
- Understanding of [basic requests](/guides/first-request)
|
|
10
|
+
|
|
11
|
+
## Understanding Rate Limits
|
|
12
|
+
|
|
13
|
+
monday.com uses a **complexity budget system** to manage API load. Every query consumes complexity points based on the data requested.
|
|
14
|
+
|
|
15
|
+
### Complexity Limits by Plan
|
|
16
|
+
|
|
17
|
+
**Per-minute limits** (sliding 60-second window):
|
|
18
|
+
|
|
19
|
+
| Token Type | Plan | Complexity Budget |
|
|
20
|
+
|------------|------|-------------------|
|
|
21
|
+
| Personal API Token | Free/Trial/NGO | 1,000,000 points |
|
|
22
|
+
| Personal API Token | Paid Plans | 10,000,000 points |
|
|
23
|
+
| App Token | All Plans | 5,000,000 (read) + 5,000,000 (write) |
|
|
24
|
+
| API Playground | Free/Trial | 1,000,000 points |
|
|
25
|
+
| API Playground | Paid Plans | 5,000,000 points |
|
|
26
|
+
|
|
27
|
+
**Per-query limit**: 5,000,000 complexity points maximum
|
|
28
|
+
|
|
29
|
+
### Daily Call Limits
|
|
30
|
+
|
|
31
|
+
Daily limits reset at **midnight UTC**:
|
|
32
|
+
|
|
33
|
+
| Plan | Daily Calls |
|
|
34
|
+
|------|-------------|
|
|
35
|
+
| Free/Trial | 200 |
|
|
36
|
+
| Standard/Basic | 1,000 |
|
|
37
|
+
| Pro | 10,000 (soft limit) |
|
|
38
|
+
| Enterprise | 25,000 (soft limit) |
|
|
39
|
+
|
|
40
|
+
**Note**: Rate-limited requests count as 0.1 calls; high-complexity queries count as 1+ calls.
|
|
41
|
+
|
|
42
|
+
### Common Query Complexity Costs
|
|
43
|
+
|
|
44
|
+
Approximate complexity points per operation:
|
|
45
|
+
|
|
46
|
+
- **Simple query** (boards list): ~500-1,000 points
|
|
47
|
+
- **Create item**: ~10,000 points
|
|
48
|
+
- **Nested query** (boards with items): ~5,000-50,000 points
|
|
49
|
+
- **Bulk operations**: Varies based on batch size
|
|
50
|
+
|
|
51
|
+
::: tip <span style="display: inline-flex; align-items: center; gap: 6px;"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>Estimate Before Creating</span>
|
|
52
|
+
When creating items in bulk, avoid creating more than 10-20 items in rapid succession to prevent hitting the complexity limit.
|
|
53
|
+
:::
|
|
54
|
+
|
|
55
|
+
## Monitor Complexity Usage
|
|
56
|
+
|
|
57
|
+
Check your complexity budget using the complexity query:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require "monday_ruby"
|
|
61
|
+
|
|
62
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
63
|
+
|
|
64
|
+
# Query with complexity field to monitor usage
|
|
65
|
+
query = <<~GRAPHQL
|
|
66
|
+
query {
|
|
67
|
+
complexity {
|
|
68
|
+
before
|
|
69
|
+
after
|
|
70
|
+
query
|
|
71
|
+
reset_in_x_seconds
|
|
72
|
+
}
|
|
73
|
+
boards(limit: 1) {
|
|
74
|
+
id
|
|
75
|
+
name
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
GRAPHQL
|
|
79
|
+
|
|
80
|
+
response = client.make_request(query)
|
|
81
|
+
|
|
82
|
+
if response.success?
|
|
83
|
+
complexity = response.body.dig("data", "complexity")
|
|
84
|
+
|
|
85
|
+
puts "Complexity Budget Status:"
|
|
86
|
+
puts " Before query: #{complexity['before']} points"
|
|
87
|
+
puts " Query cost: #{complexity['query']} points"
|
|
88
|
+
puts " After query: #{complexity['after']} points"
|
|
89
|
+
puts " Resets in: #{complexity['reset_in_x_seconds']} seconds"
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Example output:**
|
|
94
|
+
```
|
|
95
|
+
Complexity Budget Status:
|
|
96
|
+
Before query: 10000000 points
|
|
97
|
+
Query cost: 883 points
|
|
98
|
+
After query: 9999117 points
|
|
99
|
+
Resets in: 45 seconds
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Handle Rate Limit Errors
|
|
103
|
+
|
|
104
|
+
The gem raises `Monday::RateLimitError` when rate limits are exceeded:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
require "monday_ruby"
|
|
108
|
+
|
|
109
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
response = client.board.query(
|
|
113
|
+
args: { limit: 100 },
|
|
114
|
+
select: ["id", "name", { items: ["id", "name", "column_values"] }]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
boards = response.body.dig("data", "boards")
|
|
118
|
+
puts "Successfully retrieved #{boards.length} boards"
|
|
119
|
+
|
|
120
|
+
rescue Monday::RateLimitError => e
|
|
121
|
+
puts "Rate limit exceeded!"
|
|
122
|
+
puts "Error: #{e.message}"
|
|
123
|
+
puts "Error code: #{e.code}"
|
|
124
|
+
|
|
125
|
+
# Check for retry timing in error data
|
|
126
|
+
retry_seconds = e.error_data["retry_in_seconds"]
|
|
127
|
+
|
|
128
|
+
if retry_seconds
|
|
129
|
+
puts "Retry after #{retry_seconds} seconds"
|
|
130
|
+
else
|
|
131
|
+
puts "Wait 60 seconds before retrying"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Error Response Structure
|
|
137
|
+
|
|
138
|
+
Rate limit errors include helpful metadata:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
begin
|
|
142
|
+
# Expensive query that exceeds limit
|
|
143
|
+
response = client.item.create(
|
|
144
|
+
args: { board_id: 1234567890, item_name: "Test" }
|
|
145
|
+
)
|
|
146
|
+
rescue Monday::RateLimitError => e
|
|
147
|
+
# Access error details
|
|
148
|
+
puts "Message: #{e.message}"
|
|
149
|
+
# => "ComplexityException" or "Complexity budget exhausted"
|
|
150
|
+
|
|
151
|
+
puts "Code: #{e.code}"
|
|
152
|
+
# => 429
|
|
153
|
+
|
|
154
|
+
puts "Error data: #{e.error_data}"
|
|
155
|
+
# => {"retry_in_seconds"=>30} (if available)
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Retry with Exponential Backoff
|
|
160
|
+
|
|
161
|
+
Implement automatic retry logic with progressive delays:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
require "monday_ruby"
|
|
165
|
+
|
|
166
|
+
def make_request_with_retry(client, max_retries: 3)
|
|
167
|
+
retries = 0
|
|
168
|
+
base_delay = 2 # seconds
|
|
169
|
+
|
|
170
|
+
begin
|
|
171
|
+
response = yield(client)
|
|
172
|
+
return response
|
|
173
|
+
|
|
174
|
+
rescue Monday::RateLimitError => e
|
|
175
|
+
retries += 1
|
|
176
|
+
|
|
177
|
+
if retries <= max_retries
|
|
178
|
+
# Calculate exponential backoff delay
|
|
179
|
+
delay = base_delay * (2 ** (retries - 1))
|
|
180
|
+
|
|
181
|
+
# Use retry_in_seconds from API if available
|
|
182
|
+
if e.error_data["retry_in_seconds"]
|
|
183
|
+
delay = e.error_data["retry_in_seconds"]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
puts "Rate limit hit. Retry #{retries}/#{max_retries} in #{delay}s..."
|
|
187
|
+
sleep(delay)
|
|
188
|
+
retry
|
|
189
|
+
else
|
|
190
|
+
puts "Max retries exceeded. Giving up."
|
|
191
|
+
raise
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Usage
|
|
197
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
198
|
+
|
|
199
|
+
response = make_request_with_retry(client, max_retries: 3) do |c|
|
|
200
|
+
c.board.query(
|
|
201
|
+
args: { limit: 50 },
|
|
202
|
+
select: ["id", "name", { items: ["id", "name"] }]
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if response.success?
|
|
207
|
+
boards = response.body.dig("data", "boards")
|
|
208
|
+
puts "Retrieved #{boards.length} boards"
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Advanced Retry with Jitter
|
|
213
|
+
|
|
214
|
+
Add random jitter to prevent thundering herd:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
require "monday_ruby"
|
|
218
|
+
|
|
219
|
+
def exponential_backoff_with_jitter(retries, base_delay: 2, max_delay: 60)
|
|
220
|
+
# Calculate exponential backoff
|
|
221
|
+
delay = [base_delay * (2 ** (retries - 1)), max_delay].min
|
|
222
|
+
|
|
223
|
+
# Add random jitter (±25%)
|
|
224
|
+
jitter = delay * 0.25 * (rand - 0.5) * 2
|
|
225
|
+
delay + jitter
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def robust_api_call(client, max_retries: 5)
|
|
229
|
+
retries = 0
|
|
230
|
+
|
|
231
|
+
begin
|
|
232
|
+
yield(client)
|
|
233
|
+
|
|
234
|
+
rescue Monday::RateLimitError => e
|
|
235
|
+
retries += 1
|
|
236
|
+
|
|
237
|
+
if retries <= max_retries
|
|
238
|
+
delay = exponential_backoff_with_jitter(retries)
|
|
239
|
+
|
|
240
|
+
# Prefer API-provided retry timing
|
|
241
|
+
if e.error_data["retry_in_seconds"]
|
|
242
|
+
delay = e.error_data["retry_in_seconds"]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
puts "[Retry #{retries}/#{max_retries}] Waiting #{delay.round(2)}s..."
|
|
246
|
+
sleep(delay)
|
|
247
|
+
retry
|
|
248
|
+
else
|
|
249
|
+
raise
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Usage
|
|
255
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
256
|
+
|
|
257
|
+
begin
|
|
258
|
+
response = robust_api_call(client, max_retries: 5) do |c|
|
|
259
|
+
c.item.create(
|
|
260
|
+
args: {
|
|
261
|
+
board_id: 1234567890,
|
|
262
|
+
item_name: "High Priority Task"
|
|
263
|
+
},
|
|
264
|
+
select: ["id", "name"]
|
|
265
|
+
)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
item = response.body.dig("data", "create_item")
|
|
269
|
+
puts "Created item: #{item['name']} (ID: #{item['id']})"
|
|
270
|
+
|
|
271
|
+
rescue Monday::RateLimitError
|
|
272
|
+
puts "Failed after all retries due to rate limiting"
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Rate Limiting Strategies
|
|
277
|
+
|
|
278
|
+
### Add Delays Between Requests
|
|
279
|
+
|
|
280
|
+
Prevent rate limit errors by spacing out API calls:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
require "monday_ruby"
|
|
284
|
+
|
|
285
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
286
|
+
|
|
287
|
+
board_ids = [1234567890, 2345678901, 3456789012, 4567890123]
|
|
288
|
+
boards_data = []
|
|
289
|
+
|
|
290
|
+
board_ids.each_with_index do |board_id, index|
|
|
291
|
+
# Add delay after each request (except the first)
|
|
292
|
+
sleep(0.5) if index > 0
|
|
293
|
+
|
|
294
|
+
response = client.board.query(
|
|
295
|
+
args: { ids: [board_id] },
|
|
296
|
+
select: ["id", "name", { items: ["id", "name"] }]
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if response.success?
|
|
300
|
+
board = response.body.dig("data", "boards", 0)
|
|
301
|
+
boards_data << board
|
|
302
|
+
puts "Fetched board: #{board['name']}"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
puts "Total boards fetched: #{boards_data.length}"
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Batch Operations Efficiently
|
|
310
|
+
|
|
311
|
+
Group operations to reduce API calls:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
require "monday_ruby"
|
|
315
|
+
|
|
316
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
317
|
+
|
|
318
|
+
# Instead of querying boards one by one:
|
|
319
|
+
# ❌ Multiple requests (inefficient)
|
|
320
|
+
board_ids = [1234567890, 2345678901, 3456789012]
|
|
321
|
+
boards = board_ids.map do |id|
|
|
322
|
+
sleep(0.5) # Still need delays to avoid rate limits
|
|
323
|
+
response = client.board.query(args: { ids: [id] })
|
|
324
|
+
response.body.dig("data", "boards", 0)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# ✅ Single request (efficient)
|
|
328
|
+
response = client.board.query(
|
|
329
|
+
args: { ids: board_ids },
|
|
330
|
+
select: ["id", "name", "description"]
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
boards = response.body.dig("data", "boards")
|
|
334
|
+
puts "Fetched #{boards.length} boards in one request"
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Use Pagination to Control Load
|
|
338
|
+
|
|
339
|
+
Request data in smaller chunks:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
require "monday_ruby"
|
|
343
|
+
|
|
344
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
345
|
+
|
|
346
|
+
def fetch_items_paginated(client, board_id, page_size: 25)
|
|
347
|
+
all_items = []
|
|
348
|
+
page = 1
|
|
349
|
+
|
|
350
|
+
loop do
|
|
351
|
+
response = client.board.query(
|
|
352
|
+
args: { ids: [board_id] },
|
|
353
|
+
select: [
|
|
354
|
+
"id",
|
|
355
|
+
"name",
|
|
356
|
+
{
|
|
357
|
+
items_page: {
|
|
358
|
+
args: { limit: page_size, query_params: { page: page } },
|
|
359
|
+
select: ["cursor", { items: ["id", "name"] }]
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
board = response.body.dig("data", "boards", 0)
|
|
366
|
+
items_page = board.dig("items_page")
|
|
367
|
+
items = items_page["items"]
|
|
368
|
+
|
|
369
|
+
all_items.concat(items)
|
|
370
|
+
puts "Fetched page #{page}: #{items.length} items"
|
|
371
|
+
|
|
372
|
+
# Stop if we got fewer items than page size
|
|
373
|
+
break if items.length < page_size
|
|
374
|
+
|
|
375
|
+
page += 1
|
|
376
|
+
|
|
377
|
+
# Add delay between pages
|
|
378
|
+
sleep(0.5)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
all_items
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Usage
|
|
385
|
+
items = fetch_items_paginated(client, 1234567890, page_size: 50)
|
|
386
|
+
puts "Total items fetched: #{items.length}"
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Reduce Query Complexity
|
|
390
|
+
|
|
391
|
+
Request only essential fields:
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
require "monday_ruby"
|
|
395
|
+
|
|
396
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
397
|
+
|
|
398
|
+
# ❌ High complexity (requests everything)
|
|
399
|
+
response = client.board.query(
|
|
400
|
+
args: { ids: [1234567890] },
|
|
401
|
+
select: [
|
|
402
|
+
"id", "name", "description", "state", "board_folder_id",
|
|
403
|
+
{
|
|
404
|
+
items: [
|
|
405
|
+
"id", "name", "state", "created_at", "updated_at",
|
|
406
|
+
{ column_values: ["id", "text", "value", "type"] },
|
|
407
|
+
{ updates: ["id", "body", "created_at"] }
|
|
408
|
+
]
|
|
409
|
+
},
|
|
410
|
+
{ groups: ["id", "title", "color"] },
|
|
411
|
+
{ columns: ["id", "title", "type", "settings_str"] }
|
|
412
|
+
]
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# ✅ Low complexity (requests only what's needed)
|
|
416
|
+
response = client.board.query(
|
|
417
|
+
args: { ids: [1234567890] },
|
|
418
|
+
select: [
|
|
419
|
+
"id",
|
|
420
|
+
"name",
|
|
421
|
+
{ items: ["id", "name"] }
|
|
422
|
+
]
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if response.success?
|
|
426
|
+
board = response.body.dig("data", "boards", 0)
|
|
427
|
+
puts "Board: #{board['name']}, Items: #{board['items'].length}"
|
|
428
|
+
end
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Production-Ready Rate Limiter
|
|
432
|
+
|
|
433
|
+
Create a reusable rate limiter class:
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
require "monday_ruby"
|
|
437
|
+
|
|
438
|
+
class MondayRateLimiter
|
|
439
|
+
def initialize(client, requests_per_minute: 60)
|
|
440
|
+
@client = client
|
|
441
|
+
@requests_per_minute = requests_per_minute
|
|
442
|
+
@min_delay = 60.0 / requests_per_minute
|
|
443
|
+
@last_request_time = nil
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def execute(max_retries: 3, &block)
|
|
447
|
+
enforce_rate_limit
|
|
448
|
+
|
|
449
|
+
retries = 0
|
|
450
|
+
|
|
451
|
+
begin
|
|
452
|
+
response = block.call(@client)
|
|
453
|
+
@last_request_time = Time.now
|
|
454
|
+
response
|
|
455
|
+
|
|
456
|
+
rescue Monday::RateLimitError => e
|
|
457
|
+
retries += 1
|
|
458
|
+
|
|
459
|
+
if retries <= max_retries
|
|
460
|
+
delay = calculate_backoff_delay(retries, e)
|
|
461
|
+
puts "[Rate Limit] Retry #{retries}/#{max_retries} in #{delay.round(2)}s"
|
|
462
|
+
sleep(delay)
|
|
463
|
+
retry
|
|
464
|
+
else
|
|
465
|
+
raise
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
private
|
|
471
|
+
|
|
472
|
+
def enforce_rate_limit
|
|
473
|
+
return unless @last_request_time
|
|
474
|
+
|
|
475
|
+
elapsed = Time.now - @last_request_time
|
|
476
|
+
sleep_time = @min_delay - elapsed
|
|
477
|
+
|
|
478
|
+
sleep(sleep_time) if sleep_time > 0
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def calculate_backoff_delay(retries, error)
|
|
482
|
+
# Use API-provided retry timing if available
|
|
483
|
+
return error.error_data["retry_in_seconds"] if error.error_data["retry_in_seconds"]
|
|
484
|
+
|
|
485
|
+
# Otherwise use exponential backoff
|
|
486
|
+
base_delay = 2
|
|
487
|
+
max_delay = 60
|
|
488
|
+
delay = [base_delay * (2 ** (retries - 1)), max_delay].min
|
|
489
|
+
|
|
490
|
+
# Add jitter
|
|
491
|
+
jitter = delay * 0.25 * (rand - 0.5) * 2
|
|
492
|
+
delay + jitter
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Usage
|
|
497
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
498
|
+
limiter = MondayRateLimiter.new(client, requests_per_minute: 30)
|
|
499
|
+
|
|
500
|
+
# Create multiple items with rate limiting
|
|
501
|
+
item_names = ["Task 1", "Task 2", "Task 3", "Task 4", "Task 5"]
|
|
502
|
+
board_id = 1234567890
|
|
503
|
+
|
|
504
|
+
item_names.each do |item_name|
|
|
505
|
+
response = limiter.execute do |c|
|
|
506
|
+
c.item.create(
|
|
507
|
+
args: { board_id: board_id, item_name: item_name },
|
|
508
|
+
select: ["id", "name"]
|
|
509
|
+
)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
if response.success?
|
|
513
|
+
item = response.body.dig("data", "create_item")
|
|
514
|
+
puts "Created: #{item['name']}"
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Queue-Based Rate Limiting
|
|
520
|
+
|
|
521
|
+
Process requests in a queue with controlled throughput:
|
|
522
|
+
|
|
523
|
+
```ruby
|
|
524
|
+
require "monday_ruby"
|
|
525
|
+
require "thread"
|
|
526
|
+
|
|
527
|
+
class MondayRequestQueue
|
|
528
|
+
def initialize(client, max_requests_per_minute: 60)
|
|
529
|
+
@client = client
|
|
530
|
+
@queue = Queue.new
|
|
531
|
+
@max_requests_per_minute = max_requests_per_minute
|
|
532
|
+
@delay_between_requests = 60.0 / max_requests_per_minute
|
|
533
|
+
@running = false
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def start
|
|
537
|
+
return if @running
|
|
538
|
+
|
|
539
|
+
@running = true
|
|
540
|
+
@worker_thread = Thread.new { process_queue }
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def stop
|
|
544
|
+
@running = false
|
|
545
|
+
@worker_thread&.join
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def enqueue(&block)
|
|
549
|
+
result_queue = Queue.new
|
|
550
|
+
@queue << { block: block, result: result_queue }
|
|
551
|
+
result_queue.pop # Wait for result
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
private
|
|
555
|
+
|
|
556
|
+
def process_queue
|
|
557
|
+
while @running
|
|
558
|
+
begin
|
|
559
|
+
request = @queue.pop(true) # Non-blocking pop
|
|
560
|
+
|
|
561
|
+
# Execute the request
|
|
562
|
+
result = execute_with_retry(request[:block])
|
|
563
|
+
request[:result] << result
|
|
564
|
+
|
|
565
|
+
# Wait before next request
|
|
566
|
+
sleep(@delay_between_requests)
|
|
567
|
+
|
|
568
|
+
rescue ThreadError
|
|
569
|
+
# Queue is empty, sleep briefly
|
|
570
|
+
sleep(0.1)
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def execute_with_retry(block, max_retries: 3)
|
|
576
|
+
retries = 0
|
|
577
|
+
|
|
578
|
+
begin
|
|
579
|
+
block.call(@client)
|
|
580
|
+
|
|
581
|
+
rescue Monday::RateLimitError => e
|
|
582
|
+
retries += 1
|
|
583
|
+
|
|
584
|
+
if retries <= max_retries
|
|
585
|
+
delay = e.error_data["retry_in_seconds"] || (2 ** retries)
|
|
586
|
+
sleep(delay)
|
|
587
|
+
retry
|
|
588
|
+
else
|
|
589
|
+
raise
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# Usage
|
|
596
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
597
|
+
queue = MondayRequestQueue.new(client, max_requests_per_minute: 30)
|
|
598
|
+
queue.start
|
|
599
|
+
|
|
600
|
+
# Enqueue multiple requests
|
|
601
|
+
board_ids = [1234567890, 2345678901, 3456789012]
|
|
602
|
+
boards = []
|
|
603
|
+
|
|
604
|
+
board_ids.each do |board_id|
|
|
605
|
+
response = queue.enqueue do |c|
|
|
606
|
+
c.board.query(
|
|
607
|
+
args: { ids: [board_id] },
|
|
608
|
+
select: ["id", "name"]
|
|
609
|
+
)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
if response.success?
|
|
613
|
+
board = response.body.dig("data", "boards", 0)
|
|
614
|
+
boards << board
|
|
615
|
+
puts "Queued and fetched: #{board['name']}"
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
queue.stop
|
|
620
|
+
puts "Total boards: #{boards.length}"
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
## Best Practices
|
|
624
|
+
|
|
625
|
+
### 1. Monitor Your Usage
|
|
626
|
+
|
|
627
|
+
Track API calls and complexity in your application:
|
|
628
|
+
|
|
629
|
+
```ruby
|
|
630
|
+
require "monday_ruby"
|
|
631
|
+
|
|
632
|
+
class MondayMetrics
|
|
633
|
+
attr_reader :total_requests, :total_errors, :rate_limit_errors
|
|
634
|
+
|
|
635
|
+
def initialize(client)
|
|
636
|
+
@client = client
|
|
637
|
+
@total_requests = 0
|
|
638
|
+
@total_errors = 0
|
|
639
|
+
@rate_limit_errors = 0
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def execute(&block)
|
|
643
|
+
@total_requests += 1
|
|
644
|
+
start_time = Time.now
|
|
645
|
+
|
|
646
|
+
begin
|
|
647
|
+
response = block.call(@client)
|
|
648
|
+
duration = Time.now - start_time
|
|
649
|
+
|
|
650
|
+
log_request(duration, response)
|
|
651
|
+
response
|
|
652
|
+
|
|
653
|
+
rescue Monday::RateLimitError => e
|
|
654
|
+
@rate_limit_errors += 1
|
|
655
|
+
@total_errors += 1
|
|
656
|
+
log_error(e, :rate_limit)
|
|
657
|
+
raise
|
|
658
|
+
|
|
659
|
+
rescue Monday::Error => e
|
|
660
|
+
@total_errors += 1
|
|
661
|
+
log_error(e, :api_error)
|
|
662
|
+
raise
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def stats
|
|
667
|
+
{
|
|
668
|
+
total_requests: @total_requests,
|
|
669
|
+
total_errors: @total_errors,
|
|
670
|
+
rate_limit_errors: @rate_limit_errors,
|
|
671
|
+
success_rate: success_rate
|
|
672
|
+
}
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
private
|
|
676
|
+
|
|
677
|
+
def success_rate
|
|
678
|
+
return 0 if @total_requests.zero?
|
|
679
|
+
(((@total_requests - @total_errors).to_f / @total_requests) * 100).round(2)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def log_request(duration, response)
|
|
683
|
+
puts "[REQUEST] Completed in #{duration.round(3)}s - Status: #{response.status}"
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def log_error(error, type)
|
|
687
|
+
puts "[ERROR:#{type}] #{error.class}: #{error.message}"
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Usage
|
|
692
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
693
|
+
metrics = MondayMetrics.new(client)
|
|
694
|
+
|
|
695
|
+
# Make several requests
|
|
696
|
+
5.times do |i|
|
|
697
|
+
begin
|
|
698
|
+
response = metrics.execute do |c|
|
|
699
|
+
c.board.query(args: { limit: 10 })
|
|
700
|
+
end
|
|
701
|
+
puts "Request #{i + 1} succeeded"
|
|
702
|
+
rescue Monday::Error => e
|
|
703
|
+
puts "Request #{i + 1} failed: #{e.message}"
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
sleep(0.5)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
puts "\nFinal Stats:"
|
|
710
|
+
puts metrics.stats
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### 2. Cache API Responses
|
|
714
|
+
|
|
715
|
+
Reduce API calls by caching responses:
|
|
716
|
+
|
|
717
|
+
```ruby
|
|
718
|
+
require "monday_ruby"
|
|
719
|
+
|
|
720
|
+
class MondayCache
|
|
721
|
+
def initialize(client, ttl: 300)
|
|
722
|
+
@client = client
|
|
723
|
+
@cache = {}
|
|
724
|
+
@ttl = ttl # Time to live in seconds
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def get_board(board_id, select: ["id", "name"])
|
|
728
|
+
cache_key = "board_#{board_id}_#{select.hash}"
|
|
729
|
+
|
|
730
|
+
# Check cache
|
|
731
|
+
if cached = get_from_cache(cache_key)
|
|
732
|
+
puts "[CACHE HIT] Board #{board_id}"
|
|
733
|
+
return cached
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# Fetch from API
|
|
737
|
+
puts "[CACHE MISS] Fetching board #{board_id}"
|
|
738
|
+
response = @client.board.query(
|
|
739
|
+
args: { ids: [board_id] },
|
|
740
|
+
select: select
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
if response.success?
|
|
744
|
+
board = response.body.dig("data", "boards", 0)
|
|
745
|
+
set_in_cache(cache_key, board)
|
|
746
|
+
board
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def clear_cache
|
|
751
|
+
@cache.clear
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
private
|
|
755
|
+
|
|
756
|
+
def get_from_cache(key)
|
|
757
|
+
entry = @cache[key]
|
|
758
|
+
return nil unless entry
|
|
759
|
+
|
|
760
|
+
# Check if expired
|
|
761
|
+
if Time.now - entry[:timestamp] > @ttl
|
|
762
|
+
@cache.delete(key)
|
|
763
|
+
return nil
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
entry[:data]
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def set_in_cache(key, data)
|
|
770
|
+
@cache[key] = {
|
|
771
|
+
data: data,
|
|
772
|
+
timestamp: Time.now
|
|
773
|
+
}
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# Usage
|
|
778
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
779
|
+
cache = MondayCache.new(client, ttl: 600) # 10 minute cache
|
|
780
|
+
|
|
781
|
+
# First request - cache miss
|
|
782
|
+
board1 = cache.get_board(1234567890)
|
|
783
|
+
puts "Board: #{board1['name']}"
|
|
784
|
+
|
|
785
|
+
# Second request - cache hit (no API call)
|
|
786
|
+
board2 = cache.get_board(1234567890)
|
|
787
|
+
puts "Board: #{board2['name']}"
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### 3. Optimize Query Depth
|
|
791
|
+
|
|
792
|
+
Avoid deeply nested queries:
|
|
793
|
+
|
|
794
|
+
```ruby
|
|
795
|
+
require "monday_ruby"
|
|
796
|
+
|
|
797
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
798
|
+
|
|
799
|
+
# ❌ Deeply nested (high complexity)
|
|
800
|
+
response = client.board.query(
|
|
801
|
+
args: { ids: [1234567890] },
|
|
802
|
+
select: [
|
|
803
|
+
"id", "name",
|
|
804
|
+
{
|
|
805
|
+
items: [
|
|
806
|
+
"id", "name",
|
|
807
|
+
{
|
|
808
|
+
column_values: ["id", "text", "value"],
|
|
809
|
+
updates: [
|
|
810
|
+
"id", "body",
|
|
811
|
+
{ replies: ["id", "body"] }
|
|
812
|
+
]
|
|
813
|
+
}
|
|
814
|
+
]
|
|
815
|
+
}
|
|
816
|
+
]
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# ✅ Shallow queries (lower complexity)
|
|
820
|
+
# First, get the board and items
|
|
821
|
+
response1 = client.board.query(
|
|
822
|
+
args: { ids: [1234567890] },
|
|
823
|
+
select: ["id", "name", { items: ["id", "name"] }]
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
board = response1.body.dig("data", "boards", 0)
|
|
827
|
+
item_ids = board["items"].map { |i| i["id"] }
|
|
828
|
+
|
|
829
|
+
# Then, get item details separately if needed
|
|
830
|
+
response2 = client.item.query(
|
|
831
|
+
args: { ids: item_ids.take(10) }, # Process in batches
|
|
832
|
+
select: ["id", "name", { column_values: ["id", "text"] }]
|
|
833
|
+
)
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### 4. Use Environment-Based Limits
|
|
837
|
+
|
|
838
|
+
Configure rate limits based on environment:
|
|
839
|
+
|
|
840
|
+
```ruby
|
|
841
|
+
require "monday_ruby"
|
|
842
|
+
|
|
843
|
+
class MondayClientFactory
|
|
844
|
+
def self.create(environment: :production)
|
|
845
|
+
client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
|
|
846
|
+
|
|
847
|
+
config = rate_limit_config(environment)
|
|
848
|
+
MondayRateLimiter.new(client, **config)
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def self.rate_limit_config(environment)
|
|
852
|
+
case environment
|
|
853
|
+
when :production
|
|
854
|
+
{ requests_per_minute: 30 } # Conservative for production
|
|
855
|
+
when :development
|
|
856
|
+
{ requests_per_minute: 10 } # Very conservative for dev
|
|
857
|
+
when :testing
|
|
858
|
+
{ requests_per_minute: 5 } # Minimal for tests
|
|
859
|
+
else
|
|
860
|
+
{ requests_per_minute: 20 } # Default
|
|
861
|
+
end
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
# Usage
|
|
866
|
+
limiter = MondayClientFactory.create(environment: :production)
|
|
867
|
+
|
|
868
|
+
response = limiter.execute do |c|
|
|
869
|
+
c.board.query(args: { limit: 10 })
|
|
870
|
+
end
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
## Troubleshooting
|
|
874
|
+
|
|
875
|
+
### Rate Limit Exceeded Despite Delays
|
|
876
|
+
|
|
877
|
+
**Problem**: Still getting rate limit errors even with delays between requests.
|
|
878
|
+
|
|
879
|
+
**Solution**: Your queries may have high complexity. Reduce fields or use pagination:
|
|
880
|
+
|
|
881
|
+
```ruby
|
|
882
|
+
# Instead of fetching all items at once
|
|
883
|
+
response = client.board.query(
|
|
884
|
+
args: { ids: [1234567890] },
|
|
885
|
+
select: ["id", "name", { items: ["id", "name"] }]
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
# Use pagination
|
|
889
|
+
response = client.board.query(
|
|
890
|
+
args: { ids: [1234567890] },
|
|
891
|
+
select: [
|
|
892
|
+
"id", "name",
|
|
893
|
+
{
|
|
894
|
+
items_page: {
|
|
895
|
+
args: { limit: 25 },
|
|
896
|
+
select: [{ items: ["id", "name"] }]
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
]
|
|
900
|
+
)
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
### Complexity Budget Exhausted
|
|
904
|
+
|
|
905
|
+
**Problem**: Getting `COMPLEXITY_BUDGET_EXHAUSTED` error.
|
|
906
|
+
|
|
907
|
+
**Solution**: Check your remaining budget before expensive operations:
|
|
908
|
+
|
|
909
|
+
```ruby
|
|
910
|
+
query = <<~GRAPHQL
|
|
911
|
+
query {
|
|
912
|
+
complexity {
|
|
913
|
+
before
|
|
914
|
+
reset_in_x_seconds
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
GRAPHQL
|
|
918
|
+
|
|
919
|
+
response = client.make_request(query)
|
|
920
|
+
complexity = response.body.dig("data", "complexity")
|
|
921
|
+
|
|
922
|
+
if complexity["before"] < 100000
|
|
923
|
+
puts "Low budget. Waiting #{complexity['reset_in_x_seconds']}s..."
|
|
924
|
+
sleep(complexity["reset_in_x_seconds"])
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
# Now make your request
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
### Concurrent Requests Causing Errors
|
|
931
|
+
|
|
932
|
+
**Problem**: Multiple threads/processes hitting rate limits.
|
|
933
|
+
|
|
934
|
+
**Solution**: Use a centralized queue or distributed rate limiter (Redis-based):
|
|
935
|
+
|
|
936
|
+
```ruby
|
|
937
|
+
require "redis"
|
|
938
|
+
|
|
939
|
+
class DistributedRateLimiter
|
|
940
|
+
def initialize(client, redis_url: "redis://localhost:6379")
|
|
941
|
+
@client = client
|
|
942
|
+
@redis = Redis.new(url: redis_url)
|
|
943
|
+
@key = "monday_api_rate_limit"
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
def execute(&block)
|
|
947
|
+
wait_for_token
|
|
948
|
+
|
|
949
|
+
begin
|
|
950
|
+
block.call(@client)
|
|
951
|
+
ensure
|
|
952
|
+
# Record request timestamp
|
|
953
|
+
@redis.zadd(@key, Time.now.to_i, SecureRandom.uuid)
|
|
954
|
+
@redis.expire(@key, 60)
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
private
|
|
959
|
+
|
|
960
|
+
def wait_for_token
|
|
961
|
+
loop do
|
|
962
|
+
# Count requests in last 60 seconds
|
|
963
|
+
now = Time.now.to_i
|
|
964
|
+
count = @redis.zcount(@key, now - 60, now)
|
|
965
|
+
|
|
966
|
+
if count < 30 # Max 30 requests per minute
|
|
967
|
+
break
|
|
968
|
+
else
|
|
969
|
+
sleep(1)
|
|
970
|
+
end
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
end
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
## Next Steps
|
|
977
|
+
|
|
978
|
+
- [Error handling guide](/guides/advanced/errors)
|
|
979
|
+
- [Optimize query performance](/guides/boards/query)
|
|
980
|
+
- [Pagination strategies](/guides/items/query)
|
|
981
|
+
- [monday.com API rate limits documentation](https://developer.monday.com/api-reference/docs/rate-limits)
|