Skip to content

Commit 440431c

Browse files
authored
Bump tracking plan rules limit (#198)
1 parent c45cc3c commit 440431c

File tree

3 files changed

+277
-4
lines changed

3 files changed

+277
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 1.5.0 (August 11, 2025)
2+
Bump the allowed number of rules in a Tracking Plan to 2000 items depending on workspace limits.
3+
14
## 1.4.1 (July 21, 2025)
25
Bump the Go language version to `v1.24.2` to fix the following security vulnerabilities:
36

internal/provider/tracking_plan_resource.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ type trackingPlanResource struct {
3434
authContext context.Context
3535
}
3636

37+
var MaxRules = 2000
38+
3739
func (r *trackingPlanResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
3840
resp.TypeName = req.ProviderTypeName + "_tracking_plan"
3941
}
@@ -87,7 +89,7 @@ To see an exact representation of this Tracking Plan's rules, please use the dat
8789
8890
This field is currently limited to 200 items.`,
8991
Validators: []validator.Set{
90-
setvalidator.SizeAtMost(MaxPageSize),
92+
setvalidator.SizeAtMost(MaxRules),
9193
},
9294
NestedObject: schema.NestedAttributeObject{
9395
Attributes: map[string]schema.Attribute{
@@ -260,6 +262,8 @@ func (r *trackingPlanResource) Read(ctx context.Context, req resource.ReadReques
260262
}
261263
state.Rules = rules
262264
} else {
265+
outRules := []api.RuleV1{}
266+
263267
out, body, err := r.client.TrackingPlansAPI.ListRulesFromTrackingPlan(r.authContext, id).Pagination(*api.NewPaginationInput(MaxPageSize)).Execute()
264268
if body != nil {
265269
defer body.Body.Close()
@@ -273,15 +277,41 @@ func (r *trackingPlanResource) Read(ctx context.Context, req resource.ReadReques
273277
return
274278
}
275279

276-
outRules := out.Data.GetRules()
280+
outRules = append(outRules, out.Data.GetRules()...)
281+
nextPointer := out.Data.GetPagination().Next.Get()
282+
283+
for nextPointer != nil && len(outRules) < MaxRules {
284+
paginationInput := *api.NewPaginationInput(MaxPageSize)
285+
paginationInput.SetCursor(*nextPointer)
286+
287+
out, body, err = r.client.TrackingPlansAPI.ListRulesFromTrackingPlan(r.authContext, id).Pagination(paginationInput).Execute()
288+
if body != nil {
289+
defer body.Body.Close()
290+
}
291+
if err != nil {
292+
resp.Diagnostics.AddError(
293+
fmt.Sprintf("Unable to read Tracking Plan rules (ID: %s)", id),
294+
getError(err, body),
295+
)
296+
297+
return
298+
}
299+
300+
outRules = append(outRules, out.Data.GetRules()...)
301+
nextPointer = out.Data.GetPagination().Next.Get()
302+
}
303+
304+
// Limit the number of rules to MAX_RULES
305+
if len(outRules) > MaxRules {
306+
outRules = outRules[:MaxRules]
307+
}
308+
277309
err = state.Fill(trackingPlan, &outRules)
278310
if err != nil {
279311
resp.Diagnostics.AddError(
280312
"Unable to populate Tracking Plan state",
281313
err.Error(),
282314
)
283-
284-
return
285315
}
286316
}
287317

internal/provider/tracking_plan_resource_test.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package provider
22

33
import (
4+
"fmt"
45
"net/http"
56
"net/http/httptest"
7+
"regexp"
8+
"strings"
69
"testing"
710

811
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -240,3 +243,240 @@ func TestAccTrackingPlanResource(t *testing.T) {
240243
},
241244
})
242245
}
246+
247+
func TestAccTrackingPlanResource_ErrorHandling(t *testing.T) {
248+
t.Parallel()
249+
250+
t.Run("handles invalid JSON schema", func(t *testing.T) {
251+
t.Parallel()
252+
253+
fakeServer := httptest.NewServer(
254+
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
255+
w.Header().Set("content-type", "application/json")
256+
if req.URL.Path == "/tracking-plans" {
257+
_, _ = w.Write([]byte(`
258+
{
259+
"data": {
260+
"trackingPlan": {
261+
"id": "test-id",
262+
"name": "Test Plan",
263+
"type": "LIVE"
264+
}
265+
}
266+
}`))
267+
} else if req.Method == http.MethodDelete {
268+
_, _ = w.Write([]byte(`{"data": {}}`))
269+
} else {
270+
w.WriteHeader(http.StatusBadRequest)
271+
_, _ = w.Write([]byte(`{"error": "Invalid JSON schema"}`))
272+
}
273+
}),
274+
)
275+
defer fakeServer.Close()
276+
277+
providerConfig := `
278+
provider "segment" {
279+
url = "` + fakeServer.URL + `"
280+
token = "abc123"
281+
}
282+
`
283+
284+
resource.Test(t, resource.TestCase{
285+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
286+
Steps: []resource.TestStep{
287+
{
288+
Config: providerConfig + `
289+
resource "segment_tracking_plan" "test" {
290+
name = "Test Tracking Plan"
291+
type = "LIVE"
292+
rules = [
293+
{
294+
key = "Test Rule"
295+
type = "TRACK"
296+
version = 1
297+
json_schema = jsonencode({"invalid": "schema"})
298+
}
299+
]
300+
}
301+
`,
302+
ExpectError: regexp.MustCompile("Unable to create Tracking Plan rules"),
303+
},
304+
},
305+
})
306+
})
307+
}
308+
309+
func TestAccTrackingPlanResource_PaginationHandling(t *testing.T) {
310+
t.Parallel()
311+
312+
ruleRequestCount := 0
313+
fakeServer := httptest.NewServer(
314+
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
315+
w.Header().Set("content-type", "application/json")
316+
317+
if req.URL.Path == "/tracking-plans" {
318+
_, _ = w.Write([]byte(`
319+
{
320+
"data": {
321+
"trackingPlan": {
322+
"id": "test-tracking-plan-id",
323+
"name": "Test Tracking Plan",
324+
"type": "LIVE",
325+
"createdAt": "2021-11-16T00:06:19.000Z",
326+
"updatedAt": "2021-11-16T00:06:19.000Z"
327+
}
328+
}
329+
}`))
330+
} else if req.URL.Path == "/tracking-plans/test-tracking-plan-id" {
331+
_, _ = w.Write([]byte(`
332+
{
333+
"data": {
334+
"trackingPlan": {
335+
"id": "test-tracking-plan-id",
336+
"name": "Test Tracking Plan",
337+
"type": "LIVE",
338+
"createdAt": "2021-11-16T00:06:19.000Z",
339+
"updatedAt": "2021-11-16T00:06:19.000Z"
340+
}
341+
}
342+
}`))
343+
} else if req.URL.Path == "/tracking-plans/test-tracking-plan-id/rules" {
344+
ruleRequestCount++
345+
346+
// Simulate pagination - first page returns cursor, second page returns empty
347+
if ruleRequestCount == 1 {
348+
_, _ = w.Write([]byte(`
349+
{
350+
"data": {
351+
"rules": [
352+
{
353+
"key": "Rule 1",
354+
"type": "TRACK",
355+
"version": 1,
356+
"jsonSchema": {"properties": {}},
357+
"createdAt": "2023-09-08T19:02:55.000Z",
358+
"updatedAt": "2023-09-08T19:02:55.000Z"
359+
}
360+
],
361+
"pagination": {
362+
"current": "MA==",
363+
"next": "Mg==",
364+
"totalEntries": 2
365+
}
366+
}
367+
}`))
368+
} else {
369+
_, _ = w.Write([]byte(`
370+
{
371+
"data": {
372+
"rules": [
373+
{
374+
"key": "Rule 2",
375+
"type": "IDENTIFY",
376+
"version": 1,
377+
"jsonSchema": {"properties": {}},
378+
"createdAt": "2023-09-08T19:02:55.000Z",
379+
"updatedAt": "2023-09-08T19:02:55.000Z"
380+
}
381+
],
382+
"pagination": {
383+
"current": "Mg==",
384+
"totalEntries": 2
385+
}
386+
}
387+
}`))
388+
}
389+
390+
if req.Method == http.MethodPost {
391+
// Handle rule replacement
392+
_, _ = w.Write([]byte(`{"data": {}}`))
393+
}
394+
} else {
395+
_, _ = w.Write([]byte(`{"data": {}}`))
396+
}
397+
}),
398+
)
399+
defer fakeServer.Close()
400+
401+
providerConfig := `
402+
provider "segment" {
403+
url = "` + fakeServer.URL + `"
404+
token = "abc123"
405+
}
406+
`
407+
408+
resource.Test(t, resource.TestCase{
409+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
410+
Steps: []resource.TestStep{
411+
{
412+
Config: providerConfig + `
413+
resource "segment_tracking_plan" "test" {
414+
name = "Test Tracking Plan"
415+
type = "LIVE"
416+
rules = [
417+
{
418+
key = "Test Rule"
419+
type = "TRACK"
420+
version = 1
421+
json_schema = jsonencode({})
422+
}
423+
]
424+
}
425+
`,
426+
Check: resource.ComposeAggregateTestCheckFunc(
427+
resource.TestCheckResourceAttr("segment_tracking_plan.test", "id", "test-tracking-plan-id"),
428+
resource.TestCheckResourceAttr("segment_tracking_plan.test", "name", "Test Tracking Plan"),
429+
),
430+
},
431+
},
432+
})
433+
}
434+
435+
func TestAccTrackingPlanResource_MaxRulesValidation(t *testing.T) {
436+
t.Parallel()
437+
438+
fakeServer := httptest.NewServer(
439+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
440+
w.Header().Set("content-type", "application/json")
441+
_, _ = w.Write([]byte(`{"data": {}}`))
442+
}),
443+
)
444+
defer fakeServer.Close()
445+
446+
providerConfig := `
447+
provider "segment" {
448+
url = "` + fakeServer.URL + `"
449+
token = "abc123"
450+
}
451+
`
452+
453+
// Generate more than MAX_RULES (2000) rules to test validation
454+
var rulesConfig strings.Builder
455+
rulesConfig.WriteString("rules = [\n")
456+
for i := 0; i < 2001; i++ {
457+
rulesConfig.WriteString(fmt.Sprintf(`
458+
{
459+
key = "Rule %d"
460+
type = "TRACK"
461+
version = 1
462+
json_schema = jsonencode({})
463+
},`, i))
464+
}
465+
rulesConfig.WriteString("\n]")
466+
467+
resource.Test(t, resource.TestCase{
468+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
469+
Steps: []resource.TestStep{
470+
{
471+
Config: providerConfig + `
472+
resource "segment_tracking_plan" "test" {
473+
name = "Test Tracking Plan"
474+
type = "LIVE"
475+
` + rulesConfig.String() + `
476+
}
477+
`,
478+
ExpectError: regexp.MustCompile("Attribute rules set must contain at most 2000 elements"),
479+
},
480+
},
481+
})
482+
}

0 commit comments

Comments
 (0)