From 11cf117d3932d8d2ac9393f99fc8bf5796c3e08a Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Fri, 9 Jan 2026 15:01:12 +0100 Subject: [PATCH 1/2] Update to latest Loops OpenAPI spec --- .github/workflows/main.yml | 10 +- .golangci.yaml | 21 +- README.md | 51 +++-- client.go | 84 +++++++- client_test.go | 84 ++++++-- examples/contact-crud/main.go | 6 +- examples/list-transactional-email/main.go | 33 +++ examples/send-event/main.go | 5 +- examples/send-transactional-email/main.go | 9 +- models.go | 104 +++++++-- models_test.go | 4 +- testdata/create-contact-property.replay.json | 121 +++++++++++ testdata/create-contact.replay.json | 31 +-- testdata/delete-contact.replay.json | 32 +-- testdata/find-contact-by-id.replay.json | 31 +-- testdata/find-contact-not-found.replay.json | 35 ++- testdata/find-contact.replay.json | 31 +-- .../get-contact-allProperties.replay.json | 203 ++++++++++++++++++ testdata/get-contact-properties.replay.json | 64 ++++++ testdata/get-custom-fields.replay.json | 35 ++- .../get-dedicated-sending-ips.replay.json | 118 ++++++++++ testdata/get-mailing-lists.replay.json | 31 +-- .../list-transactional-emails.replay.json | 118 ++++++++++ testdata/send-event.replay.json | 35 ++- testdata/update-contact.replay.json | 31 +-- 25 files changed, 1067 insertions(+), 260 deletions(-) create mode 100644 examples/list-transactional-email/main.go create mode 100644 testdata/create-contact-property.replay.json create mode 100644 testdata/get-contact-allProperties.replay.json create mode 100644 testdata/get-contact-properties.replay.json create mode 100644 testdata/get-dedicated-sending-ips.replay.json create mode 100644 testdata/list-transactional-emails.replay.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 757ba99..72a42e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,16 +13,16 @@ jobs: strategy: matrix: os: [Ubuntu] - go-version: ["1.24.x"] + go-version: ["1.25.x"] runs-on: ${{ matrix.os }}-latest permissions: contents: read # for golangci-lint-action steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: lfs: true - name: Setup Go ${{ matrix.go-version }} - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - name: Install dependencies @@ -39,6 +39,6 @@ jobs: paths: "test-report.xml" if: always() - name: Lint - uses: golangci/golangci-lint-action@v7 + uses: golangci/golangci-lint-action@v9 with: - version: v2.0.1 + version: v2.8.0 diff --git a/.golangci.yaml b/.golangci.yaml index 687e4e9..6244e4a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,7 +2,7 @@ --- version: "2" linters: - enable: # list taken from https://golangci-lint.run/usage/linters/ - last updated 2025-02-14 for v1.64.5 + enable: # list taken from https://golangci-lint.run/usage/linters/ - last updated 2026-01-09 for v2.8.0 # enabled by default, but list them here to be explicit - errcheck - govet @@ -10,6 +10,7 @@ linters: - staticcheck - unused # other linters (that would be disabled by default) + - arangolint # linter for arangoDB - asasalint # checks for pass []any as any in variadic func(...any) - asciicheck # checks that your code does not contain non-ASCII identifiers - bidichk # checks for dangerous unicode character sequences @@ -25,6 +26,7 @@ linters: #- dupl # tool for code clone detection - dupword # checks for duplicate words in the source code - durationcheck # checks for two durations multiplied together + - embeddedstructfieldcheck # embedded types should be at the top of a struct #- err113 # checks the errors handling expressions - errchkjson # checks types passed to the json encoding functions. - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error @@ -35,6 +37,7 @@ linters: - fatcontext # finds nested context.WithValue calls in loops - forbidigo # forbids identifiers #- forcetypeassert # finds forced type assertions + - funcorder # checks that functions are in the right order #- funlen # Tool for detection of long functions #- ginkgolinter # Enforces the Ginkgo testing package guidelines. - gocheckcompilerdirectives # validates go compiler directive comments (//go:) @@ -45,6 +48,7 @@ linters: - goconst # finds repeated strings that could be replaced by a constant - gocritic # provides diagnostics that check for bugs, performance and style issues #- gocyclo # Computes and checks the cyclomatic complexity of functions + - godoclint # Checks golang docs best practices (godoc) #- godot # Check if comments end in a period #- godox # Tool for detection of FIXME, TODO and other comment keywords #- goheader # Checks is file header matches to pattern @@ -59,6 +63,7 @@ linters: #- inamedparam # Reports interfaces with unnamed method parameters. - interfacebloat # Checks the number of methods in an interface - intrange # finds places where for loops could make use of an integer range + - iotamixing # checks that iota is not mixed with other constants #- ireturn # Accept Interfaces, Return Concrete Types #- lll # Reports long lines - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) @@ -67,6 +72,7 @@ linters: - mirror # reports wrong mirror patterns of bytes/strings usage - misspell # finds commonly misspelled English words in comments #- mnd # Detects magic numbers + - modernize # reports wrong mirror patterns of bytes/strings usage - musttag # enforces field tags in (un)marshaled structs - nakedret # finds naked returns in functions greater than a specified function length #- nestif # Reports deeply nested if statements @@ -74,13 +80,13 @@ linters: - nilnesserr # Reports constructs that checks for err != nil, but returns a different nil value error. - nilnil # checks that there is no simultaneous return of nil error and an invalid value #- nlreturn # Checks for a new line before return and branch statements to increase code clarity + #- noinlineerr # Disallows inline error handling - noctx # finds sending http request without context.Context - nolintlint # reports ill-formed or insufficient nolint directives - nonamedreturns # reports all named returns - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL - - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative #- paralleltest # detects missing usage of t.Parallel() method in your Go test - - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative - prealloc # finds slice declarations that could potentially be preallocated - predeclared # finds code that shadows one of Go's predeclared identifiers - promlinter # checks Prometheus metrics naming via promlint @@ -101,15 +107,16 @@ linters: - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes - unconvert # removes unnecessary type conversions - unparam # reports unused function parameters + - unqueryvet # sql linter disallowing select * - usestdlibvars # detects the possibility to use variables/constants from the Go standard library - usetesting # reports uses of functions with replacement inside the testing package #- varnamelen # checks that the length of a variable's name matches its scope - wastedassign # finds wasted assignment statements - whitespace # detects leading and trailing whitespace #- wrapcheck # Checks that errors returned from external packages are wrapped - #- wsl # whitespace linter - add or remove empty lines + #- wsl # (deprecated) + #- wsl_v5 # whitespace linter - add or remove empty lines #- zerologlint # checks wrong usage of zerolog - #- tenv # deprecated in favor of usetesting settings: gosec: excludes: @@ -147,7 +154,7 @@ linters: strict: false exclusions: generated: strict - warn-unused: true + warn-unused: false presets: - comments - common-false-positives @@ -164,4 +171,4 @@ formatters: - gci - gofmt - gofumpt - - goimports + - goimports \ No newline at end of file diff --git a/README.md b/README.md index 843564b..b76382a 100644 --- a/README.md +++ b/README.md @@ -40,17 +40,6 @@ func main() { ### Contacts -**Find a contact** -```go -contact, err := client.FindContact(ctx, &loops.ContactIdentifier{ - Email: loops.String("neil.armstrong@moon.space"), -}) -if err != nil { - slog.Error("failed to find contact", slog.Any("error", err.Error())) - return -} -``` - **Create a contact** ```go contactID, err := client.CreateContact(ctx, &loops.Contact{ @@ -60,7 +49,7 @@ contactID, err := client.CreateContact(ctx, &loops.Contact{ UserGroup: loops.String("Astronauts"), Subscribed: true, // custom user defined properties for contacts - CustomProperties: map[string]interface{}{ + Properties: map[string]interface{}{ "role": "Astronaut", }, }) @@ -70,6 +59,17 @@ if err != nil { } ``` +**Find a contact** +```go +contact, err := client.FindContact(ctx, &loops.ContactIdentifier{ + Email: loops.String("neil.armstrong@moon.space"), +}) +if err != nil { + slog.Error("failed to find contact", slog.Any("error", err.Error())) + return +} +``` + **Delete a contact** ```go err = client.DeleteContact(ctx, &loops.ContactIdentifier{ @@ -81,6 +81,17 @@ if err != nil { } ``` +**List contact properties** +```go +properties, err := client.GetContactProperties(ctx, loops.ContactPropertyListOptions{ + List: loops.ContactPropertyTypeCustom, // only return your teams custom properties +}) +if err != nil { + slog.Error("failed to get contact properties", slog.Any("error", err.Error())) + return +} +``` + ### Events **Send an event** @@ -116,6 +127,22 @@ if err != nil { } ``` +**List transactional emails** + +```go +emailsPage, err := client.ListTransactionalEmails(ctx, loops.ListTransactionalEmailsOptions{ + PerPage: 10, +}) +if err != nil { + slog.Error("failed to list transactional emails", slog.Any("error", err.Error())) + return +} + +for _, email := range emailsPage.Data { + slog.Info("transactional email", slog.String("id", email.ID), slog.String("name", email.Name)) +} +``` + ## API Documentation The API documentation is part of the official Loops Documentation and can be found [here](https://app.loops.so/docs/api-reference/). diff --git a/client.go b/client.go index d4050d6..b7548b9 100644 --- a/client.go +++ b/client.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "net/url" + "strconv" ) const defaultURL = "https://app.loops.so/api/v1/" @@ -220,17 +221,92 @@ func (c *Client) SendTransactionalEmail(ctx context.Context, transactional *Tran return err } -// GetCustomFields retrieves a list of an account's custom contact properties. -func (c *Client) GetCustomFields(ctx context.Context) ([]*CustomField, error) { +type ContactPropertyType int + +const ( + ContactPropertyTypeAll ContactPropertyType = iota + ContactPropertyTypeCustom +) + +type ContactPropertyListOptions struct { + // Which contact properties to return (all or custom to only list your team's custom properties) + List ContactPropertyType +} + +// GetContactProperties retrieves a list of an account's contact properties. +// Use listType "all" (default) or "custom" to filter properties. +// See: https://loops.so/docs/api-reference/list-contact-properties +func (c *Client) GetContactProperties(ctx context.Context, opts ContactPropertyListOptions) ([]*ContactProperty, error) { + params := url.Values{} + if opts.List == ContactPropertyTypeCustom { + params.Add("list", "custom") + } else if opts.List != ContactPropertyTypeAll { + return nil, errors.New("invalid list type") + } + req, err := newGetRequestWithQueryParams(c, ctx, "/contacts/properties", params) + if err != nil { + return nil, err + } + return sendRequest[[]*ContactProperty](c, req) +} + +// CreateContactProperty creates a new contact property. +// See: https://loops.so/docs/api-reference/create-contact-property +func (c *Client) CreateContactProperty(ctx context.Context, property *ContactPropertyCreate) error { + req, err := newRequestWithBody(c, ctx, http.MethodPost, "/contacts/properties", property) + if err != nil { + return err + } + _, err = sendRequest[*SuccessResponse](c, req) + return err +} + +// Deprecated: Use GetContactProperties instead. +func (c *Client) GetCustomFields(ctx context.Context) ([]*ContactProperty, error) { req, err := newGetRequestWithQueryParams(c, ctx, "/contacts/customFields", nil) if err != nil { return nil, err } - customFields, err := sendRequest[[]*CustomField](c, req) + return sendRequest[[]*ContactProperty](c, req) +} + +// GetDedicatedSendingIPs retrieves a list of Loops' dedicated sending IP addresses. +// See: https://loops.so/docs/api-reference/list-dedicated-sending-ips +func (c *Client) GetDedicatedSendingIPs(ctx context.Context) ([]string, error) { + req, err := newGetRequestWithQueryParams(c, ctx, "/dedicated-sending-ips", nil) + if err != nil { + return nil, err + } + return sendRequest[[]string](c, req) +} + +type ListTransactionalEmailsOptions struct { + // Number of results per page (10-50, default 20) + PerPage int + // Pagination cursor from previous response + Cursor string +} + +// ListTransactionalEmails retrieves a list of published transactional emails. +// perPage: number of results per page (10-50, default 20) +// cursor: pagination cursor from previous response +// See: https://loops.so/docs/api-reference/list-transactional-emails +func (c *Client) ListTransactionalEmails(ctx context.Context, opts ListTransactionalEmailsOptions) (*TransactionalEmailList, error) { + params := url.Values{} + if opts.PerPage != 0 { + if opts.PerPage < 10 || opts.PerPage > 50 { + return nil, errors.New("perPage must be between 10 and 50 (inclusive)") + } + params.Add("perPage", strconv.Itoa(opts.PerPage)) + } + if opts.Cursor != "" { + params.Add("cursor", opts.Cursor) + } + req, err := newGetRequestWithQueryParams(c, ctx, "/transactional", params) if err != nil { return nil, err } - return customFields, nil + return sendRequest[*TransactionalEmailList](c, req) } // TestAPIKey tests that an API key is valid. diff --git a/client_test.go b/client_test.go index 75ad89e..d77b26b 100644 --- a/client_test.go +++ b/client_test.go @@ -51,12 +51,12 @@ func TestCreateContact(t *testing.T) { LastName: String("User"), UserID: String("user_123"), Subscribed: true, - CustomProperties: map[string]interface{}{ + Properties: map[string]any{ "companyRole": "Developer", }, }) require.NoError(t, err) - assert.Equal(t, "cm3n4kiua02c0t839btycnwe1", contactID) + assert.Equal(t, "cmk6vyub00c7b0i04dlregeit", contactID) } func TestUpdateContact(t *testing.T) { @@ -69,7 +69,7 @@ func TestUpdateContact(t *testing.T) { Subscribed: true, }) require.NoError(t, err) - assert.Equal(t, "cm3n4kiua02c0t839btycnwe1", contactID) + assert.Equal(t, "cmk6vyub00c7b0i04dlregeit", contactID) } func TestFindContact(t *testing.T) { @@ -78,13 +78,14 @@ func TestFindContact(t *testing.T) { Email: String("new-test-mail@example.com"), }) require.NoError(t, err) - assert.Equal(t, "cm3n4kiua02c0t839btycnwe1", contact.ID) + assert.Equal(t, "cmk6vyub00c7b0i04dlregeit", contact.ID) assert.Equal(t, "new-test-mail@example.com", contact.Email) assert.Equal(t, "Test", *contact.FirstName) assert.Equal(t, "User", *contact.LastName) assert.Equal(t, "user_123", *contact.UserID) + assert.Nil(t, contact.OptInStatus) - companyRole, ok := contact.CustomProperties["companyRole"] + companyRole, ok := contact.Properties["companyRole"] assert.True(t, ok) companyRoleStr, ok := companyRole.(string) assert.True(t, ok) @@ -99,13 +100,14 @@ func TestFindContactByID(t *testing.T) { UserID: String("user_123"), }) require.NoError(t, err) - assert.Equal(t, "cm3n4kiua02c0t839btycnwe1", contact.ID) + assert.Equal(t, "cmk6vyub00c7b0i04dlregeit", contact.ID) assert.Equal(t, "new-test-mail@example.com", contact.Email) assert.Equal(t, "Test", *contact.FirstName) assert.Equal(t, "User", *contact.LastName) assert.Equal(t, "user_123", *contact.UserID) + assert.Nil(t, contact.OptInStatus) - companyRole, ok := contact.CustomProperties["companyRole"] + companyRole, ok := contact.Properties["companyRole"] assert.True(t, ok) companyRoleStr, ok := companyRole.(string) assert.True(t, ok) @@ -123,6 +125,36 @@ func TestFindContactNotFound(t *testing.T) { assert.Contains(t, err.Error(), "contact not found") } +func TestGetContactProperties(t *testing.T) { + client := newReplayTestClient(t, "get-contact-allProperties.replay.json") + allProperties, err := client.GetContactProperties(context.Background(), ContactPropertyListOptions{}) + require.NoError(t, err) + require.Len(t, allProperties, 14) + assert.Equal(t, "firstName", allProperties[0].Key) + assert.Equal(t, "First Name", allProperties[0].Label) + assert.Equal(t, "string", allProperties[0].Type) + assert.Equal(t, "lastName", allProperties[1].Key) + + customProperties, err := client.GetContactProperties(context.Background(), ContactPropertyListOptions{ + List: ContactPropertyTypeCustom, + }) + require.NoError(t, err) + require.Len(t, customProperties, 2) + assert.Equal(t, "heardAboutChannel", customProperties[0].Key) + assert.Equal(t, "Heard About Channel", customProperties[0].Label) + assert.Equal(t, "string", customProperties[0].Type) + assert.Equal(t, "companyRole", customProperties[1].Key) +} + +func TestCreateContactProperty(t *testing.T) { + client := newReplayTestClient(t, "create-contact-property.replay.json") + err := client.CreateContactProperty(context.Background(), &ContactPropertyCreate{ + Name: "planName", + Type: "string", + }) + require.NoError(t, err) +} + func TestDeleteContact(t *testing.T) { client := newReplayTestClient(t, "delete-contact.replay.json") err := client.DeleteContact(context.Background(), &ContactIdentifier{ @@ -135,10 +167,13 @@ func TestGetMailingLists(t *testing.T) { client := newReplayTestClient(t, "get-mailing-lists.replay.json") mailingLists, err := client.GetMailingLists(context.Background()) require.NoError(t, err) - require.Len(t, mailingLists, 1) + require.Len(t, mailingLists, 2) assert.Equal(t, "cm3n274xf027h0mi33t4qhrdg", mailingLists[0].ID) assert.Equal(t, "Newsletter", mailingLists[0].Name) assert.True(t, mailingLists[0].IsPublic) + assert.Equal(t, "cm6gb0ku002d00kiig98e153r", mailingLists[1].ID) + assert.Equal(t, "Product Update", mailingLists[1].Name) + assert.True(t, mailingLists[1].IsPublic) } func TestSendEvent(t *testing.T) { @@ -146,7 +181,7 @@ func TestSendEvent(t *testing.T) { err := client.SendEvent(context.Background(), &Event{ Email: String("neil.armstrong@moon.space"), EventName: "joinedMission", - EventProperties: &map[string]interface{}{ + EventProperties: &map[string]any{ "mission": "Apollo 11", }, }) @@ -158,21 +193,36 @@ func TestSendTransactionalEmail(t *testing.T) { err := client.SendTransactionalEmail(context.Background(), &TransactionalEmail{ TransactionalID: "cm3n2vjux00cgeyeflew9ly2w", Email: "test@example.com", - DataVariables: &map[string]interface{}{ + DataVariables: &map[string]any{ "name": "Mr. Test", }, }) require.NoError(t, err) } -func TestGetCustomFields(t *testing.T) { - client := newReplayTestClient(t, "get-custom-fields.replay.json") - customFields, err := client.GetCustomFields(context.Background()) +func TestGetDedicatedSendingIPs(t *testing.T) { + client := newReplayTestClient(t, "get-dedicated-sending-ips.replay.json") + ips, err := client.GetDedicatedSendingIPs(context.Background()) + require.NoError(t, err) + require.Len(t, ips, 5) + assert.Contains(t, ips[0], "221.169") +} + +func TestListTransactionalEmails(t *testing.T) { + client := newReplayTestClient(t, "list-transactional-emails.replay.json") + response, err := client.ListTransactionalEmails(context.Background(), ListTransactionalEmailsOptions{}) require.NoError(t, err) - require.Len(t, customFields, 1) - assert.Equal(t, "role", customFields[0].Key) - assert.Equal(t, "Role", customFields[0].Label) - assert.Equal(t, "string", customFields[0].Type) + assert.Equal(t, 2, response.Pagination.TotalResults) + assert.Equal(t, 2, response.Pagination.ReturnedResults) + assert.Equal(t, 20, response.Pagination.PerPage) + assert.Equal(t, 1, response.Pagination.TotalPages) + assert.Empty(t, response.Pagination.NextCursor) + assert.Empty(t, response.Pagination.NextPage) + require.Len(t, response.Data, 2) + assert.Equal(t, "cm3n2vjux00cgeyeflew9ly2w", response.Data[0].ID) + assert.Equal(t, "Blank transactional", response.Data[0].Name) + assert.Equal(t, "2024-11-18T14:32:35.586Z", response.Data[0].LastUpdated) + assert.Equal(t, []string{"name"}, response.Data[0].DataVariables) } func TestAPIKey(t *testing.T) { diff --git a/examples/contact-crud/main.go b/examples/contact-crud/main.go index cc34209..9ff12de 100644 --- a/examples/contact-crud/main.go +++ b/examples/contact-crud/main.go @@ -3,11 +3,14 @@ package main import ( "context" "log/slog" + "os" "github.com/tilebox/loops-go" ) func main() { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))) + client, err := loops.NewClient(loops.WithAPIKey("YOUR_LOOPS_API_KEY")) if err != nil { slog.Error("failed to create client", slog.Any("error", err.Error())) @@ -22,7 +25,8 @@ func main() { FirstName: loops.String("Neil"), LastName: loops.String("Armstrong"), Subscribed: true, - CustomProperties: map[string]interface{}{ // custom user defined properties for contacts + // custom user defined properties for contacts + Properties: map[string]interface{}{ "role": "Astronaut", }, }) diff --git a/examples/list-transactional-email/main.go b/examples/list-transactional-email/main.go new file mode 100644 index 0000000..7101d96 --- /dev/null +++ b/examples/list-transactional-email/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "context" + "log/slog" + "os" + + "github.com/tilebox/loops-go" +) + +func main() { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))) + + client, err := loops.NewClient(loops.WithAPIKey("YOUR_LOOPS_API_KEY")) + if err != nil { + slog.Error("failed to create client", slog.Any("error", err.Error())) + return + } + + ctx := context.Background() + emailsPage, err := client.ListTransactionalEmails(ctx, loops.ListTransactionalEmailsOptions{ + PerPage: 10, + }) + if err != nil { + slog.Error("failed to list transactional emails", slog.Any("error", err.Error())) + return + } + slog.Info("transactional emails summary", slog.Int("count", len(emailsPage.Data))) + + for _, email := range emailsPage.Data { + slog.Info("transactional email", slog.String("id", email.ID), slog.String("name", email.Name)) + } +} diff --git a/examples/send-event/main.go b/examples/send-event/main.go index 8d0a447..d7c875a 100644 --- a/examples/send-event/main.go +++ b/examples/send-event/main.go @@ -3,11 +3,14 @@ package main import ( "context" "log/slog" + "os" "github.com/tilebox/loops-go" ) func main() { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))) + client, err := loops.NewClient(loops.WithAPIKey("YOUR_LOOPS_API_KEY")) if err != nil { slog.Error("failed to create client", slog.Any("error", err.Error())) @@ -19,7 +22,7 @@ func main() { err = client.SendEvent(ctx, &loops.Event{ Email: loops.String("neil.armstrong@moon.space"), EventName: "joinedMission", - EventProperties: &map[string]interface{}{ + EventProperties: &map[string]any{ "mission": "Apollo 11", }, }) diff --git a/examples/send-transactional-email/main.go b/examples/send-transactional-email/main.go index 3b67def..5f4efa5 100644 --- a/examples/send-transactional-email/main.go +++ b/examples/send-transactional-email/main.go @@ -3,11 +3,14 @@ package main import ( "context" "log/slog" + "os" "github.com/tilebox/loops-go" ) func main() { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))) + client, err := loops.NewClient(loops.WithAPIKey("YOUR_LOOPS_API_KEY")) if err != nil { slog.Error("failed to create client", slog.Any("error", err.Error())) @@ -18,9 +21,9 @@ func main() { err = client.SendTransactionalEmail(ctx, &loops.TransactionalEmail{ TransactionalID: "cm3n2vjux00cgeyeflew9ly2w", - Email: "lukas.bindreiter@tilebox.com", - DataVariables: &map[string]interface{}{ - "name": "Mr. Lukas", + Email: "neil.armstrong@moon.space", + DataVariables: &map[string]any{ + "name": "Mr. Armstrong", }, }) if err != nil { diff --git a/models.go b/models.go index 15912da..1a23ec2 100644 --- a/models.go +++ b/models.go @@ -3,6 +3,7 @@ package loops import ( "encoding/json" "errors" + "maps" ) // String returns a pointer to the string value passed in. @@ -10,6 +11,15 @@ func String(v string) *string { return &v } +// OptInStatus represents the double opt-in status of a contact. +type OptInStatus string + +const ( + OptInStatusAccepted OptInStatus = "accepted" + OptInStatusPending OptInStatus = "pending" + OptInStatusRejected OptInStatus = "rejected" +) + // Contact defines model for Contact. type Contact struct { // The contact's ID. @@ -30,13 +40,15 @@ type Contact struct { UserID *string `json:"userId,omitempty"` // Mailing lists the contact is subscribed to. MailingLists map[string]bool `json:"mailingLists,omitempty"` + // Double opt-in status. + OptInStatus *OptInStatus `json:"optInStatus,omitempty"` // Custom properties for the contact. - CustomProperties map[string]interface{} `json:"-"` // there is no "customProperties", we need to inline add them to the json + Properties map[string]any `json:"-"` // there is no "customProperties", we need to inline add them to the json } // MarshalJSON overrides the default json marshaller to add custom properties inline to the root object func (c *Contact) MarshalJSON() ([]byte, error) { - data := map[string]interface{}{ + data := map[string]any{ "id": c.ID, "email": c.Email, "subscribed": c.Subscribed, @@ -59,15 +71,16 @@ func (c *Contact) MarshalJSON() ([]byte, error) { if c.MailingLists != nil { data["mailingLists"] = c.MailingLists } - for k, v := range c.CustomProperties { - data[k] = v + if c.OptInStatus != nil { + data["optInStatus"] = *c.OptInStatus } + maps.Copy(data, c.Properties) return json.Marshal(data) } // UnmarshalJSON overrides the default json unmarshaller to add custom properties inline to the root object func (c *Contact) UnmarshalJSON(data []byte) error { - values := map[string]interface{}{} + values := map[string]any{} if err := json.Unmarshal(data, &values); err != nil { return err } @@ -118,7 +131,7 @@ func (c *Contact) UnmarshalJSON(data []byte) error { delete(values, "userId") } - mailingLists, ok := values["mailingLists"].(map[string]interface{}) + mailingLists, ok := values["mailingLists"].(map[string]any) if ok { c.MailingLists = make(map[string]bool) for k, v := range mailingLists { @@ -127,10 +140,14 @@ func (c *Contact) UnmarshalJSON(data []byte) error { delete(values, "mailingLists") } - c.CustomProperties = make(map[string]interface{}) - for k, v := range values { - c.CustomProperties[k] = v + if optInStatus, ok := values["optInStatus"].(string); ok { + status := OptInStatus(optInStatus) + c.OptInStatus = &status + delete(values, "optInStatus") } + + c.Properties = make(map[string]any) + maps.Copy(c.Properties, values) return nil } @@ -139,11 +156,32 @@ type ContactIdentifier struct { UserID *string `json:"userId,omitempty"` } +type ContactProperty struct { + // The property's name key + Key string `json:"key"` + // The human-friendly label for this property + Label string `json:"label"` + // The type of property (one of string, number, boolean or date) + Type string `json:"type"` +} + +// Deprecated: Use ContactProperty instead. +type CustomField = ContactProperty + +type ContactPropertyCreate struct { + // The property's name key (must be in camelCase, like `planName`) + Name string `json:"name"` + // The type of property (one of string, number, boolean or date) + Type string `json:"type"` +} + type MailingList struct { // The ID of the list. ID string `json:"id"` // The name of the list. Name string `json:"name"` + // The description of the list. + Description string `json:"description"` // Whether the list is public (true) or private (false). // See: https://loops.so/docs/contacts/mailing-lists#list-visibility IsPublic bool `json:"isPublic"` @@ -157,11 +195,11 @@ type Event struct { // The name of the event EventName string `json:"eventName"` // Properties to update the contact with, including custom properties. - ContactProperties map[string]interface{} `json:"contactProperties,omitempty"` + ContactProperties map[string]any `json:"contactProperties,omitempty"` // Event properties, made available in emails triggered by the event. - EventProperties *map[string]interface{} `json:"eventProperties,omitempty"` + EventProperties *map[string]any `json:"eventProperties,omitempty"` // An object of mailing list IDs and boolean subscription statuses. - MailingLists *map[string]interface{} `json:"mailingLists,omitempty"` + MailingLists *map[string]any `json:"mailingLists,omitempty"` } type TransactionalEmail struct { @@ -172,7 +210,7 @@ type TransactionalEmail struct { // Create a contact in your audience using the provided email address (if one doesn't already exist). AddToAudience *bool `json:"addToAudience,omitempty"` // Data variables as defined by the transitional email template. - DataVariables *map[string]interface{} `json:"dataVariables,omitempty"` + DataVariables *map[string]any `json:"dataVariables,omitempty"` // File(s) to be sent along with the email message. Attachments *[]EmailAttachment `json:"attachments,omitempty"` } @@ -186,13 +224,35 @@ type EmailAttachment struct { Data string `json:"data"` } -type CustomField struct { - // The property's name key - Key string `json:"key"` - // The human-friendly label for this property - Label string `json:"label"` - // The type of property (one of string, number, boolean or date) - Type string `json:"type"` +type TransactionalEmailInfo struct { + // The ID of the transactional email. + ID string `json:"id"` + // The name of the transactional email. + Name string `json:"name"` + // The last time the transactional email was updated. + LastUpdated string `json:"lastUpdated"` + // The data variables used in the transactional email. + DataVariables []string `json:"dataVariables"` +} + +type TransactionalEmailList struct { + Data []*TransactionalEmailInfo `json:"data"` + Pagination Pagination `json:"pagination"` +} + +type Pagination struct { + // Total results found. + TotalResults int `json:"totalResults"` + // The number of results returned in this response. + ReturnedResults int `json:"returnedResults"` + // The maximum number of results requested. + PerPage int `json:"perPage"` + // Total number of pages. + TotalPages int `json:"totalPages"` + // The next cursor (for retrieving the next page of results using the cursor parameter), or empty string if there are no further pages. + NextCursor string `json:"nextCursor,omitempty"` + // The next page (for retrieving the next page of results using the page parameter), or empty string if there are no further pages. + NextPage string `json:"nextPage,omitempty"` } type APIKeyInfo struct { @@ -205,6 +265,10 @@ type errorResponse struct { Error string `json:"error"` } +type SuccessResponse struct { + Success bool `json:"success"` +} + type IDResponse struct { Success bool `json:"success"` ID string `json:"id"` diff --git a/models_test.go b/models_test.go index a8b80ff..f93ec3d 100644 --- a/models_test.go +++ b/models_test.go @@ -16,7 +16,7 @@ func TestContactMarshalJSONCustomPropertiesInlined(t *testing.T) { MailingLists: map[string]bool{ "list_123": true, }, - CustomProperties: map[string]interface{}{ + Properties: map[string]any{ "favoriteColor": "blue", }, } @@ -34,7 +34,7 @@ func TestContactUnmarshalJSONCustomPropertiesInlined(t *testing.T) { assert.Equal(t, "123", c.ID) assert.Equal(t, "test@example.com", c.Email) assert.True(t, c.Subscribed) - assert.Equal(t, "blue", c.CustomProperties["favoriteColor"]) + assert.Equal(t, "blue", c.Properties["favoriteColor"]) assert.Equal(t, "John", *c.FirstName) assert.Equal(t, "Doe", *c.LastName) require.Len(t, c.MailingLists, 1) diff --git a/testdata/create-contact-property.replay.json b/testdata/create-contact-property.replay.json new file mode 100644 index 0000000..0af7c78 --- /dev/null +++ b/testdata/create-contact-property.replay.json @@ -0,0 +1,121 @@ +{ + "Initial": null, + "Version": "0.2", + "Converter": { + "ScrubBody": null, + "ClearHeaders": [ + "^X-Goog-.*Encryption-Key$" + ], + "RemoveRequestHeaders": [ + "^Authorization$", + "^Proxy-Authorization$", + "^Connection$", + "^Content-Type$", + "^Date$", + "^Host$", + "^Transfer-Encoding$", + "^Via$", + "^X-Forwarded-.*$", + "^X-Cloud-Trace-Context$", + "^X-Goog-Api-Client$", + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "RemoveResponseHeaders": [ + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "ClearParams": null, + "RemoveParams": null + }, + "Entries": [ + { + "ID": "0bb350a3449309bc", + "Request": { + "Method": "POST", + "URL": "https://app.loops.so/api/v1/contacts/properties", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Length": [ + "35" + ], + "User-Agent": [ + "Go-http-client/1.1" + ] + }, + "MediaType": "application/json", + "BodyParts": [ + "eyJuYW1lIjoicGxhbk5hbWUiLCJ0eXBlIjoic3RyaW5nIn0=" + ] + }, + "Response": { + "StatusCode": 200, + "Proto": "HTTP/1.1", + "ProtoMajor": 1, + "ProtoMinor": 1, + "Header": { + "Access-Control-Allow-Credentials": [ + "true" + ], + "Access-Control-Allow-Headers": [ + "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" + ], + "Access-Control-Allow-Methods": [ + "GET,OPTIONS,PATCH,DELETE,POST,PUT" + ], + "Access-Control-Allow-Origin": [ + "*" + ], + "Cf-Cache-Status": [ + "DYNAMIC" + ], + "Cf-Ray": [ + "9bb4499bae9a5db9-VIE" + ], + "Content-Encoding": [ + "gzip" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Fri, 09 Jan 2026 13:30:08 GMT" + ], + "Etag": [ + "W/\"17a6zzdutk1g\"" + ], + "Permissions-Policy": [ + "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" + ], + "Referrer-Policy": [ + "strict-origin-when-cross-origin" + ], + "Server": [ + "cloudflare" + ], + "Strict-Transport-Security": [ + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-Ratelimit-Limit": [ + "10" + ], + "X-Ratelimit-Remaining": [ + "9" + ], + "X-Robots-Tag": [ + "none" + ] + }, + "Body": "H4sIAAAAAAAA/6tWKi5NTk4tLlayKikqTa0FAN/39mYQAAAA" + } + } + ] +} \ No newline at end of file diff --git a/testdata/create-contact.replay.json b/testdata/create-contact.replay.json index 4742d2a..e9c6813 100644 --- a/testdata/create-contact.replay.json +++ b/testdata/create-contact.replay.json @@ -30,7 +30,7 @@ }, "Entries": [ { - "ID": "f9c686557906d4a1", + "ID": "0e65ffdf712f4699", "Request": { "Method": "POST", "URL": "https://app.loops.so/api/v1/contacts/create", @@ -68,29 +68,23 @@ "Access-Control-Allow-Origin": [ "*" ], - "Cache-Control": [ - "public, max-age=0, must-revalidate" - ], "Cf-Cache-Status": [ "DYNAMIC" ], "Cf-Ray": [ - "8e48dffe6dce5aaf-VIE" + "9bb4210c7cdfafe9-VIE" ], "Content-Encoding": [ "gzip" ], - "Content-Security-Policy": [ - "default-src 'self'; connect-src 'self' https://loops-prod-mjml-bucket.s3.amazonaws.com https://loops-prod-csv-uploads.s3.amazonaws.com https://vitals.vercel-insights.com https://browser-intake-datadoghq.com https://*.google-analytics.com https://api.zapier.com https://app.atlas.so https://app.getatlas.io wss://app.atlas.so https://*.filestackapi.com https://atlas-prod-uploads.s3.amazonaws.com https://ipgeolocation.abstractapi.com https://zapier.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.zapier.com https://www.googletagmanager.com https://app.getatlas.io https://app.atlas.so https://static.filestackapi.com https://files.atlas.so; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.zapier.com https://static.filestackapi.com; img-src 'self' blob: data: https://d3b9kr64nievew.cloudfront.net https://atlas-prod-uploads.s3.amazonaws.com https://files.getatlas.io https://files.atlas.so https://static.filestackapi.com https://cdn.filestackcontent.com https://zapier-images.imgix.net; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-src 'self' https://zapier.com; media-src 'self' data:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" - ], "Content-Type": [ "application/json; charset=utf-8" ], "Date": [ - "Mon, 18 Nov 2024 15:08:18 GMT" + "Fri, 09 Jan 2026 13:02:27 GMT" ], "Etag": [ - "W/\"oai0fa27xa1d\"" + "W/\"v3jrx4hjv1d\"" ], "Permissions-Policy": [ "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" @@ -102,28 +96,25 @@ "cloudflare" ], "Strict-Transport-Security": [ - "max-age=63072000" + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" ], "X-Content-Type-Options": [ "nosniff" ], - "X-Matched-Path": [ - "/api/v1/contacts/create" - ], "X-Ratelimit-Limit": [ "10" ], "X-Ratelimit-Remaining": [ "9" ], - "X-Vercel-Cache": [ - "MISS" - ], - "X-Vercel-Id": [ - "fra1::iad1::p46k7-1731942497167-d5c57a669dd8" + "X-Robots-Tag": [ + "none" ] }, - "Body": "H4sIAAAAAAAAA6pWKi5NTk4tLlayKikqTdVRykxRslJKzjXOM8nOLE00MEo2KLEwtkwqqUzOK081VKoFAAAA//8DAKOElQ0xAAAA" + "Body": "H4sIAAAAAAAA/wExAM7/eyJzdWNjZXNzIjp0cnVlLCJpZCI6ImNtazZ2eXViMDBjN2IwaTA0ZGxyZWdlaXQifdgt8TAxAAAA" } } ] diff --git a/testdata/delete-contact.replay.json b/testdata/delete-contact.replay.json index 193d241..93c6159 100644 --- a/testdata/delete-contact.replay.json +++ b/testdata/delete-contact.replay.json @@ -30,7 +30,7 @@ }, "Entries": [ { - "ID": "024bf65228970fa0", + "ID": "9d6196b8be0c4e95", "Request": { "Method": "POST", "URL": "https://app.loops.so/api/v1/contacts/delete", @@ -68,29 +68,23 @@ "Access-Control-Allow-Origin": [ "*" ], - "Cache-Control": [ - "public, max-age=0, must-revalidate" - ], "Cf-Cache-Status": [ "DYNAMIC" ], "Cf-Ray": [ - "8e48e25f7eabc2f5-VIE" - ], - "Content-Length": [ - "45" + "9bb451d4bfd01479-VIE" ], - "Content-Security-Policy": [ - "default-src 'self'; connect-src 'self' https://loops-prod-mjml-bucket.s3.amazonaws.com https://loops-prod-csv-uploads.s3.amazonaws.com https://vitals.vercel-insights.com https://browser-intake-datadoghq.com https://*.google-analytics.com https://api.zapier.com https://app.atlas.so https://app.getatlas.io wss://app.atlas.so https://*.filestackapi.com https://atlas-prod-uploads.s3.amazonaws.com https://ipgeolocation.abstractapi.com https://zapier.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.zapier.com https://www.googletagmanager.com https://app.getatlas.io https://app.atlas.so https://static.filestackapi.com https://files.atlas.so; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.zapier.com https://static.filestackapi.com; img-src 'self' blob: data: https://d3b9kr64nievew.cloudfront.net https://atlas-prod-uploads.s3.amazonaws.com https://files.getatlas.io https://files.atlas.so https://static.filestackapi.com https://cdn.filestackcontent.com https://zapier-images.imgix.net; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-src 'self' https://zapier.com; media-src 'self' data:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" + "Content-Encoding": [ + "gzip" ], "Content-Type": [ "application/json; charset=utf-8" ], "Date": [ - "Mon, 18 Nov 2024 15:09:55 GMT" + "Fri, 09 Jan 2026 13:35:45 GMT" ], "Etag": [ - "\"ah8yi3hmwo19\"" + "W/\"pljev2gncl18\"" ], "Permissions-Policy": [ "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" @@ -102,7 +96,7 @@ "cloudflare" ], "Strict-Transport-Security": [ - "max-age=63072000" + "max-age=31536000; includeSubDomains" ], "Vary": [ "Accept-Encoding" @@ -110,23 +104,17 @@ "X-Content-Type-Options": [ "nosniff" ], - "X-Matched-Path": [ - "/api/v1/contacts/delete" - ], "X-Ratelimit-Limit": [ "10" ], "X-Ratelimit-Remaining": [ "9" ], - "X-Vercel-Cache": [ - "MISS" - ], - "X-Vercel-Id": [ - "fra1::iad1::tlxlc-1731942594542-0551c093e4b8" + "X-Robots-Tag": [ + "none" ] }, - "Body": "eyJzdWNjZXNzIjp0cnVlLCJtZXNzYWdlIjoiQ29udGFjdCBkZWxldGVkLiJ9" + "Body": "H4sIAAAAAAAA/wEsANP/eyJzdWNjZXNzIjp0cnVlLCJtZXNzYWdlIjoiQ29udGFjdCBkZWxldGVkIn1ftOYZLAAAAA==" } } ] diff --git a/testdata/find-contact-by-id.replay.json b/testdata/find-contact-by-id.replay.json index 71d9185..f1f7873 100644 --- a/testdata/find-contact-by-id.replay.json +++ b/testdata/find-contact-by-id.replay.json @@ -30,7 +30,7 @@ }, "Entries": [ { - "ID": "7b58e36ca41e99b2", + "ID": "37a24473d7181ac0", "Request": { "Method": "GET", "URL": "https://app.loops.so/api/v1/contacts/find?userId=user_123", @@ -65,29 +65,23 @@ "Access-Control-Allow-Origin": [ "*" ], - "Cache-Control": [ - "public, max-age=0, must-revalidate" - ], "Cf-Cache-Status": [ "DYNAMIC" ], "Cf-Ray": [ - "8e48e21068c35afd-VIE" + "9bb451708d0f1479-VIE" ], "Content-Encoding": [ "gzip" ], - "Content-Security-Policy": [ - "default-src 'self'; connect-src 'self' https://loops-prod-mjml-bucket.s3.amazonaws.com https://loops-prod-csv-uploads.s3.amazonaws.com https://vitals.vercel-insights.com https://browser-intake-datadoghq.com https://*.google-analytics.com https://api.zapier.com https://app.atlas.so https://app.getatlas.io wss://app.atlas.so https://*.filestackapi.com https://atlas-prod-uploads.s3.amazonaws.com https://ipgeolocation.abstractapi.com https://zapier.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.zapier.com https://www.googletagmanager.com https://app.getatlas.io https://app.atlas.so https://static.filestackapi.com https://files.atlas.so; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.zapier.com https://static.filestackapi.com; img-src 'self' blob: data: https://d3b9kr64nievew.cloudfront.net https://atlas-prod-uploads.s3.amazonaws.com https://files.getatlas.io https://files.atlas.so https://static.filestackapi.com https://cdn.filestackcontent.com https://zapier-images.imgix.net; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-src 'self' https://zapier.com; media-src 'self' data:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" - ], "Content-Type": [ "application/json; charset=utf-8" ], "Date": [ - "Mon, 18 Nov 2024 15:09:42 GMT" + "Fri, 09 Jan 2026 13:35:29 GMT" ], "Etag": [ - "W/\"xtzfail1ta65\"" + "W/\"n8rs4l5qf6o\"" ], "Permissions-Policy": [ "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" @@ -99,28 +93,25 @@ "cloudflare" ], "Strict-Transport-Security": [ - "max-age=63072000" + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" ], "X-Content-Type-Options": [ "nosniff" ], - "X-Matched-Path": [ - "/api/v1/contacts/find" - ], "X-Ratelimit-Limit": [ "10" ], "X-Ratelimit-Remaining": [ "9" ], - "X-Vercel-Cache": [ - "BYPASS" - ], - "X-Vercel-Id": [ - "fra1::iad1::sqgz4-1731942581847-7210d258c83c" + "X-Robots-Tag": [ + "none" ] }, - "Body": "H4sIAAAAAAAAAzyOTQtBQRhG/8uzHrpcC2ZFKSlJYiVp7nhpMh+3eWdckv+uoeye8yxO5/CCOUNCu9qPbiaraqirNK4nTXpq39EAAuSUsZDw1PUSceoVntJDudZSXwcHgYuJnNbKESR2xAkCVv2fPVOEAIccdeHZZlkwN6yjaegMmWImgcwUFzHkFhL44bLUlXEaDGsI6OBa5Z/bYItoTneyof3aS5Xx15XhxJCv9/v4AQAA//8DABnyOWXdAAAA" + "Body": "H4sIAAAAAAAA/z2OywrCMBBF/2XWVeoDBVcKghRExMdKRNI4SjAvkolaSv/diYK7uYfDnXtqQV1hBtI8Js8m1WUpp3WpyvFVB7yjIigAjVCaHYuvHmGkXs5zfAvjNfalM+zcVIi0EQbZO7DDSIs/OUYMTKJLQea82FY5pjrKoGrkARQSFpDYWwWXPDvwi1Vel4/LYDhixu+8sM3O6Vy0xCdq57/teZWy97WKFGHWdgU4T5Xdk6DEwCatu/MH9XnW5/AAAAA=" } } ] diff --git a/testdata/find-contact-not-found.replay.json b/testdata/find-contact-not-found.replay.json index 7f15cfb..e6f62f5 100644 --- a/testdata/find-contact-not-found.replay.json +++ b/testdata/find-contact-not-found.replay.json @@ -30,7 +30,7 @@ }, "Entries": [ { - "ID": "d7d84445c833bc47", + "ID": "b211794fc7143b4b", "Request": { "Method": "GET", "URL": "https://app.loops.so/api/v1/contacts/find?userId=not_found", @@ -65,29 +65,23 @@ "Access-Control-Allow-Origin": [ "*" ], - "Cache-Control": [ - "public, max-age=0, must-revalidate" - ], "Cf-Cache-Status": [ "DYNAMIC" ], "Cf-Ray": [ - "8e48e24b2ac43257-VIE" - ], - "Content-Length": [ - "2" + "9bb451ac580651d5-VIE" ], - "Content-Security-Policy": [ - "default-src 'self'; connect-src 'self' https://loops-prod-mjml-bucket.s3.amazonaws.com https://loops-prod-csv-uploads.s3.amazonaws.com https://vitals.vercel-insights.com https://browser-intake-datadoghq.com https://*.google-analytics.com https://api.zapier.com https://app.atlas.so https://app.getatlas.io wss://app.atlas.so https://*.filestackapi.com https://atlas-prod-uploads.s3.amazonaws.com https://ipgeolocation.abstractapi.com https://zapier.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.zapier.com https://www.googletagmanager.com https://app.getatlas.io https://app.atlas.so https://static.filestackapi.com https://files.atlas.so; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.zapier.com https://static.filestackapi.com; img-src 'self' blob: data: https://d3b9kr64nievew.cloudfront.net https://atlas-prod-uploads.s3.amazonaws.com https://files.getatlas.io https://files.atlas.so https://static.filestackapi.com https://cdn.filestackcontent.com https://zapier-images.imgix.net; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-src 'self' https://zapier.com; media-src 'self' data:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" + "Content-Encoding": [ + "gzip" ], "Content-Type": [ "application/json; charset=utf-8" ], "Date": [ - "Mon, 18 Nov 2024 15:09:51 GMT" + "Fri, 09 Jan 2026 13:35:39 GMT" ], "Etag": [ - "\"38jmpejbxv2\"" + "W/\"38jmpejbxv2\"" ], "Permissions-Policy": [ "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" @@ -99,28 +93,25 @@ "cloudflare" ], "Strict-Transport-Security": [ - "max-age=63072000" + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" ], "X-Content-Type-Options": [ "nosniff" ], - "X-Matched-Path": [ - "/api/v1/contacts/find" - ], "X-Ratelimit-Limit": [ "10" ], "X-Ratelimit-Remaining": [ "9" ], - "X-Vercel-Cache": [ - "BYPASS" - ], - "X-Vercel-Id": [ - "fra1::iad1::24n79-1731942591260-3e2d63340fb6" + "X-Robots-Tag": [ + "none" ] }, - "Body": "W10=" + "Body": "H4sIAAAAAAAA/4uOBQApu0wNAgAAAA==" } } ] diff --git a/testdata/find-contact.replay.json b/testdata/find-contact.replay.json index da38d0b..4e9bcee 100644 --- a/testdata/find-contact.replay.json +++ b/testdata/find-contact.replay.json @@ -30,7 +30,7 @@ }, "Entries": [ { - "ID": "4cf850fbb69ba59e", + "ID": "a47a7bcf26d3ba62", "Request": { "Method": "GET", "URL": "https://app.loops.so/api/v1/contacts/find?email=new-test-mail%40example.com", @@ -65,29 +65,23 @@ "Access-Control-Allow-Origin": [ "*" ], - "Cache-Control": [ - "public, max-age=0, must-revalidate" - ], "Cf-Cache-Status": [ "DYNAMIC" ], "Cf-Ray": [ - "8e48e1a2ca155ae2-VIE" + "9bb423820f64b855-VIE" ], "Content-Encoding": [ "gzip" ], - "Content-Security-Policy": [ - "default-src 'self'; connect-src 'self' https://loops-prod-mjml-bucket.s3.amazonaws.com https://loops-prod-csv-uploads.s3.amazonaws.com https://vitals.vercel-insights.com https://browser-intake-datadoghq.com https://*.google-analytics.com https://api.zapier.com https://app.atlas.so https://app.getatlas.io wss://app.atlas.so https://*.filestackapi.com https://atlas-prod-uploads.s3.amazonaws.com https://ipgeolocation.abstractapi.com https://zapier.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.zapier.com https://www.googletagmanager.com https://app.getatlas.io https://app.atlas.so https://static.filestackapi.com https://files.atlas.so; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.zapier.com https://static.filestackapi.com; img-src 'self' blob: data: https://d3b9kr64nievew.cloudfront.net https://atlas-prod-uploads.s3.amazonaws.com https://files.getatlas.io https://files.atlas.so https://static.filestackapi.com https://cdn.filestackcontent.com https://zapier-images.imgix.net; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-src 'self' https://zapier.com; media-src 'self' data:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" - ], "Content-Type": [ "application/json; charset=utf-8" ], "Date": [ - "Mon, 18 Nov 2024 15:09:24 GMT" + "Fri, 09 Jan 2026 13:04:08 GMT" ], "Etag": [ - "W/\"xtzfail1ta65\"" + "W/\"n8rs4l5qf6o\"" ], "Permissions-Policy": [ "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" @@ -99,28 +93,25 @@ "cloudflare" ], "Strict-Transport-Security": [ - "max-age=63072000" + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" ], "X-Content-Type-Options": [ "nosniff" ], - "X-Matched-Path": [ - "/api/v1/contacts/find" - ], "X-Ratelimit-Limit": [ "10" ], "X-Ratelimit-Remaining": [ "9" ], - "X-Vercel-Cache": [ - "BYPASS" - ], - "X-Vercel-Id": [ - "fra1::iad1::n7t7w-1731942564318-f11fae1cd594" + "X-Robots-Tag": [ + "none" ] }, - "Body": "H4sIAAAAAAAAAzyOTQtBQRhG/8uzHrpcC2ZFKSlJYiVp7nhpMh+3eWdckv+uoeye8yxO5/CCOUNCu9qPbiaraqirNK4nTXpq39EAAuSUsZDw1PUSceoVntJDudZSXwcHgYuJnNbKESR2xAkCVv2fPVOEAIccdeHZZlkwN6yjaegMmWImgcwUFzHkFhL44bLUlXEaDGsI6OBa5Z/bYItoTneyof3aS5Xx15XhxJCv9/v4AQAA//8DABnyOWXdAAAA" + "Body": "H4sIAAAAAAAA/z2OywrCMBBF/2XWVeoDBVcKghRExMdKRNI4SjAvkolaSv/diYK7uYfDnXtqQV1hBtI8Js8m1WUpp3WpyvFVB7yjIigAjVCaHYuvHmGkXs5zfAvjNfalM+zcVIi0EQbZO7DDSIs/OUYMTKJLQea82FY5pjrKoGrkARQSFpDYWwWXPDvwi1Vel4/LYDhixu+8sM3O6Vy0xCdq57/teZWy97WKFGHWdgU4T5Xdk6DEwCatu/MH9XnW5/AAAAA=" } } ] diff --git a/testdata/get-contact-allProperties.replay.json b/testdata/get-contact-allProperties.replay.json new file mode 100644 index 0000000..04fbab6 --- /dev/null +++ b/testdata/get-contact-allProperties.replay.json @@ -0,0 +1,203 @@ +{ + "Initial": null, + "Version": "0.2", + "Converter": { + "ScrubBody": null, + "ClearHeaders": [ + "^X-Goog-.*Encryption-Key$" + ], + "RemoveRequestHeaders": [ + "^Authorization$", + "^Proxy-Authorization$", + "^Connection$", + "^Content-Type$", + "^Date$", + "^Host$", + "^Transfer-Encoding$", + "^Via$", + "^X-Forwarded-.*$", + "^X-Cloud-Trace-Context$", + "^X-Goog-Api-Client$", + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "RemoveResponseHeaders": [ + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "ClearParams": null, + "RemoveParams": null + }, + "Entries": [ + { + "ID": "33d797cb0d2e846a", + "Request": { + "Method": "GET", + "URL": "https://app.loops.so/api/v1/contacts/properties", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "User-Agent": [ + "Go-http-client/1.1" + ] + }, + "MediaType": "application/json", + "BodyParts": [ + "" + ] + }, + "Response": { + "StatusCode": 200, + "Proto": "HTTP/1.1", + "ProtoMajor": 1, + "ProtoMinor": 1, + "Header": { + "Access-Control-Allow-Credentials": [ + "true" + ], + "Access-Control-Allow-Headers": [ + "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" + ], + "Access-Control-Allow-Methods": [ + "GET,OPTIONS,PATCH,DELETE,POST,PUT" + ], + "Access-Control-Allow-Origin": [ + "*" + ], + "Cf-Cache-Status": [ + "DYNAMIC" + ], + "Cf-Ray": [ + "9bb4513c6899a464-VIE" + ], + "Content-Encoding": [ + "gzip" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Fri, 09 Jan 2026 13:35:21 GMT" + ], + "Etag": [ + "W/\"ov7r6awch2my\"" + ], + "Permissions-Policy": [ + "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" + ], + "Referrer-Policy": [ + "strict-origin-when-cross-origin" + ], + "Server": [ + "cloudflare" + ], + "Strict-Transport-Security": [ + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-Ratelimit-Limit": [ + "10" + ], + "X-Ratelimit-Remaining": [ + "9" + ], + "X-Robots-Tag": [ + "none" + ] + }, + "Body": "H4sIAAAAAAAA/4WRy2rDMBBFf0VonS/oLoS+oGTR0FUpZiRNGxFZMnosTOm/V3ZkOkk9ZKlz5wjN1fu3POEo7+SnjSnvoUe5kQ4UusoeJiYazOOAlaUcrf+SP5tFdPDPe4HbGvZgHXHu25mb9yFjIvP7dubmu86EeqUnCkGcVRLGZ0OctwrETDglFZV0tAqpdqCwmSoEh+CJqiNCRrPNxNydmZhhM00lRDsiRLNVoeTdEbxHWuPTlIk5FH8pX1IP8YS50g6Kseg1XhS2GrP7TO09xlCG6wIXyL1Dh34AP77WC2kVZyoaZj8glHjx7MMC+LXr32Srwa1vvZZeL/3xC6SXXDs6AwAA" + } + }, + { + "ID": "9dfeb47f40fc2993", + "Request": { + "Method": "GET", + "URL": "https://app.loops.so/api/v1/contacts/properties?list=custom", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "User-Agent": [ + "Go-http-client/1.1" + ] + }, + "MediaType": "application/json", + "BodyParts": [ + "" + ] + }, + "Response": { + "StatusCode": 200, + "Proto": "HTTP/1.1", + "ProtoMajor": 1, + "ProtoMinor": 1, + "Header": { + "Access-Control-Allow-Credentials": [ + "true" + ], + "Access-Control-Allow-Headers": [ + "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" + ], + "Access-Control-Allow-Methods": [ + "GET,OPTIONS,PATCH,DELETE,POST,PUT" + ], + "Access-Control-Allow-Origin": [ + "*" + ], + "Cf-Cache-Status": [ + "DYNAMIC" + ], + "Cf-Ray": [ + "9bb4513d4a51a464-VIE" + ], + "Content-Encoding": [ + "gzip" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Fri, 09 Jan 2026 13:35:21 GMT" + ], + "Etag": [ + "W/\"zayry85csl3s\"" + ], + "Permissions-Policy": [ + "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" + ], + "Referrer-Policy": [ + "strict-origin-when-cross-origin" + ], + "Server": [ + "cloudflare" + ], + "Strict-Transport-Security": [ + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-Ratelimit-Limit": [ + "10" + ], + "X-Ratelimit-Remaining": [ + "8" + ], + "X-Robots-Tag": [ + "none" + ] + }, + "Body": "H4sIAAAAAAAA/12MvQqAIBRGX0Xu7BO4hUtzazRoXTKyq5gNEr179kfR9ME5fKdeYcQEAgyq0BXaLVEaRYQWOFil8wooD8dOyV4bk8cs5xgG6mHjT6l1k1eUKmfx05AXZTf+nZsdYXNg1ogAAAA=" + } + } + ] +} \ No newline at end of file diff --git a/testdata/get-contact-properties.replay.json b/testdata/get-contact-properties.replay.json new file mode 100644 index 0000000..2ac02a0 --- /dev/null +++ b/testdata/get-contact-properties.replay.json @@ -0,0 +1,64 @@ +{ + "Initial": null, + "Version": "0.2", + "Converter": { + "ScrubBody": null, + "ClearHeaders": [ + "^X-Goog-.*Encryption-Key$" + ], + "RemoveRequestHeaders": [ + "^Authorization$", + "^Proxy-Authorization$", + "^Connection$", + "^Content-Type$", + "^Date$", + "^Host$", + "^Transfer-Encoding$", + "^Via$", + "^X-Forwarded-.*$", + "^X-Cloud-Trace-Context$", + "^X-Goog-Api-Client$", + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "RemoveResponseHeaders": [ + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "ClearParams": null, + "RemoveParams": null + }, + "Entries": [ + { + "ID": "get-contact-properties-1", + "Request": { + "Method": "GET", + "URL": "https://app.loops.so/api/v1/contacts/properties?list=custom", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "User-Agent": [ + "Go-http-client/1.1" + ] + }, + "MediaType": "application/json", + "BodyParts": [ + "" + ] + }, + "Response": { + "StatusCode": 200, + "Proto": "HTTP/1.1", + "ProtoMajor": 1, + "ProtoMinor": 1, + "Header": { + "Content-Type": [ + "application/json; charset=utf-8" + ] + }, + "Body": "W3sia2V5IjoiZmF2b3JpdGVDb2xvciIsImxhYmVsIjoiRmF2b3JpdGUgY29sb3IiLCJ0eXBlIjoic3RyaW5nIn0seyJrZXkiOiJwbGFuTmFtZSIsImxhYmVsIjoiUGxhbiBuYW1lIiwidHlwZSI6InN0cmluZyJ9XQ==" + } + } + ] +} diff --git a/testdata/get-custom-fields.replay.json b/testdata/get-custom-fields.replay.json index 4937a36..34772fd 100644 --- a/testdata/get-custom-fields.replay.json +++ b/testdata/get-custom-fields.replay.json @@ -30,7 +30,7 @@ }, "Entries": [ { - "ID": "365c7f36ca48117a", + "ID": "892663838e15e538", "Request": { "Method": "GET", "URL": "https://app.loops.so/api/v1/contacts/customFields", @@ -65,29 +65,23 @@ "Access-Control-Allow-Origin": [ "*" ], - "Cache-Control": [ - "public, max-age=0, must-revalidate" - ], "Cf-Cache-Status": [ "DYNAMIC" ], "Cf-Ray": [ - "8e48ae1a4b8d5ab3-VIE" - ], - "Content-Length": [ - "47" + "9bb42840dd434341-VIE" ], - "Content-Security-Policy": [ - "default-src 'self'; connect-src 'self' https://loops-prod-mjml-bucket.s3.amazonaws.com https://loops-prod-csv-uploads.s3.amazonaws.com https://vitals.vercel-insights.com https://browser-intake-datadoghq.com https://*.google-analytics.com https://api.zapier.com https://app.atlas.so https://app.getatlas.io wss://app.atlas.so https://*.filestackapi.com https://atlas-prod-uploads.s3.amazonaws.com https://ipgeolocation.abstractapi.com https://zapier.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.zapier.com https://www.googletagmanager.com https://app.getatlas.io https://app.atlas.so https://static.filestackapi.com https://files.atlas.so; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.zapier.com https://static.filestackapi.com; img-src 'self' blob: data: https://d3b9kr64nievew.cloudfront.net https://atlas-prod-uploads.s3.amazonaws.com https://files.getatlas.io https://files.atlas.so https://static.filestackapi.com https://cdn.filestackcontent.com https://zapier-images.imgix.net; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-src 'self' https://zapier.com; media-src 'self' data:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" + "Content-Encoding": [ + "gzip" ], "Content-Type": [ "application/json; charset=utf-8" ], "Date": [ - "Mon, 18 Nov 2024 14:34:14 GMT" + "Fri, 09 Jan 2026 13:07:22 GMT" ], "Etag": [ - "\"j3urs9lnhp1b\"" + "W/\"bcyfrshchl3s\"" ], "Permissions-Policy": [ "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" @@ -99,28 +93,25 @@ "cloudflare" ], "Strict-Transport-Security": [ - "max-age=63072000" + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" ], "X-Content-Type-Options": [ "nosniff" ], - "X-Matched-Path": [ - "/api/v1/contacts/customFields" - ], "X-Ratelimit-Limit": [ "10" ], "X-Ratelimit-Remaining": [ "9" ], - "X-Vercel-Cache": [ - "BYPASS" - ], - "X-Vercel-Id": [ - "fra1::iad1::5v8sq-1731940453508-d2267dd70bb6" + "X-Robots-Tag": [ + "none" ] }, - "Body": "W3sia2V5Ijoicm9sZSIsImxhYmVsIjoiUm9sZSIsInR5cGUiOiJzdHJpbmcifV0=" + "Body": "H4sIAAAAAAAA/12MOQ6AIBAAv0K29gV2hsba1lgsuhEjLgSxIMa/i1dIbGcy0+4wU4QSers45NhYQ1CAQUUmUflQ8eIQHSW6Bj/xCEfxxZrQD5WyW5AamVOaF/XlxC1Ftr9TdwJW7cYJiAAAAA==" } } ] diff --git a/testdata/get-dedicated-sending-ips.replay.json b/testdata/get-dedicated-sending-ips.replay.json new file mode 100644 index 0000000..ff5997f --- /dev/null +++ b/testdata/get-dedicated-sending-ips.replay.json @@ -0,0 +1,118 @@ +{ + "Initial": null, + "Version": "0.2", + "Converter": { + "ScrubBody": null, + "ClearHeaders": [ + "^X-Goog-.*Encryption-Key$" + ], + "RemoveRequestHeaders": [ + "^Authorization$", + "^Proxy-Authorization$", + "^Connection$", + "^Content-Type$", + "^Date$", + "^Host$", + "^Transfer-Encoding$", + "^Via$", + "^X-Forwarded-.*$", + "^X-Cloud-Trace-Context$", + "^X-Goog-Api-Client$", + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "RemoveResponseHeaders": [ + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "ClearParams": null, + "RemoveParams": null + }, + "Entries": [ + { + "ID": "59ee636d1ddfbe1f", + "Request": { + "Method": "GET", + "URL": "https://app.loops.so/api/v1/dedicated-sending-ips", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "User-Agent": [ + "Go-http-client/1.1" + ] + }, + "MediaType": "application/json", + "BodyParts": [ + "" + ] + }, + "Response": { + "StatusCode": 200, + "Proto": "HTTP/1.1", + "ProtoMajor": 1, + "ProtoMinor": 1, + "Header": { + "Access-Control-Allow-Credentials": [ + "true" + ], + "Access-Control-Allow-Headers": [ + "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" + ], + "Access-Control-Allow-Methods": [ + "GET,OPTIONS,PATCH,DELETE,POST,PUT" + ], + "Access-Control-Allow-Origin": [ + "*" + ], + "Cf-Cache-Status": [ + "DYNAMIC" + ], + "Cf-Ray": [ + "9bb442e64f81b654-VIE" + ], + "Content-Encoding": [ + "gzip" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Fri, 09 Jan 2026 13:25:34 GMT" + ], + "Etag": [ + "W/\"ufpu51e7bw2c\"" + ], + "Permissions-Policy": [ + "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" + ], + "Referrer-Policy": [ + "strict-origin-when-cross-origin" + ], + "Server": [ + "cloudflare" + ], + "Strict-Transport-Security": [ + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-Ratelimit-Limit": [ + "10" + ], + "X-Ratelimit-Remaining": [ + "9" + ], + "X-Robots-Tag": [ + "none" + ] + }, + "Body": "H4sIAAAAAAAA/xWMwQ3AMAgDd+FdWdghJMxSdf81Cj/f2fJrYkIimAX6scd2QOFYBXk0nxkssAWn76zNds15RwRIx52Dsu8HI/5UoFQAAAA=" + } + } + ] +} \ No newline at end of file diff --git a/testdata/get-mailing-lists.replay.json b/testdata/get-mailing-lists.replay.json index 1445c91..2c4e426 100644 --- a/testdata/get-mailing-lists.replay.json +++ b/testdata/get-mailing-lists.replay.json @@ -30,7 +30,7 @@ }, "Entries": [ { - "ID": "eddf18e63363fd6f", + "ID": "0d34c2f7c51f0e85", "Request": { "Method": "GET", "URL": "https://app.loops.so/api/v1/lists", @@ -65,29 +65,23 @@ "Access-Control-Allow-Origin": [ "*" ], - "Cache-Control": [ - "public, max-age=0, must-revalidate" - ], "Cf-Cache-Status": [ "DYNAMIC" ], "Cf-Ray": [ - "8e48a831c9fdc2e6-VIE" + "9bb447ff8a7cc275-VIE" ], "Content-Encoding": [ "gzip" ], - "Content-Security-Policy": [ - "default-src 'self'; connect-src 'self' https://loops-prod-mjml-bucket.s3.amazonaws.com https://loops-prod-csv-uploads.s3.amazonaws.com https://vitals.vercel-insights.com https://browser-intake-datadoghq.com https://*.google-analytics.com https://api.zapier.com https://app.atlas.so https://app.getatlas.io wss://app.atlas.so https://*.filestackapi.com https://atlas-prod-uploads.s3.amazonaws.com https://ipgeolocation.abstractapi.com https://zapier.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.zapier.com https://www.googletagmanager.com https://app.getatlas.io https://app.atlas.so https://static.filestackapi.com https://files.atlas.so; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.zapier.com https://static.filestackapi.com; img-src 'self' blob: data: https://d3b9kr64nievew.cloudfront.net https://atlas-prod-uploads.s3.amazonaws.com https://files.getatlas.io https://files.atlas.so https://static.filestackapi.com https://cdn.filestackcontent.com https://zapier-images.imgix.net; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-src 'self' https://zapier.com; media-src 'self' data:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" - ], "Content-Type": [ "application/json; charset=utf-8" ], "Date": [ - "Mon, 18 Nov 2024 14:30:12 GMT" + "Fri, 09 Jan 2026 13:29:02 GMT" ], "Etag": [ - "W/\"qi65y4heca20\"" + "W/\"x2h0kktws955\"" ], "Permissions-Policy": [ "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" @@ -99,28 +93,25 @@ "cloudflare" ], "Strict-Transport-Security": [ - "max-age=63072000" + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" ], "X-Content-Type-Options": [ "nosniff" ], - "X-Matched-Path": [ - "/api/v1/lists" - ], "X-Ratelimit-Limit": [ "10" ], "X-Ratelimit-Remaining": [ "9" ], - "X-Vercel-Cache": [ - "BYPASS" - ], - "X-Vercel-Id": [ - "fra1::iad1::t8krc-1731940211522-c499eca72d1e" + "X-Robots-Tag": [ + "none" ] }, - "Body": "H4sIAAAAAAAAA4quVspMUbJSSs41zjMyN6lIMzAyzzDIzTQ2LjEpzChKSVfSUcpLzE1VslLySy0vzkktKUktUtJRyiwOKE3KyUxWsiopKk2tjQUAAAD//wMATtATwUgAAAA=" + "Body": "H4sIAAAAAAAA/43MsQ6CMBCA4VcxNzOcFEX7EIbFyThAe5YLbcH2Gk2M7y6Trq5//nyXF7AFDSaoWLfN84Z1O2JgpaS5j8k6qCD2gdblRI/sSYTS2ixlk3gRniPoWLyvgHNXBs8GtKRC7+or792AU0GsLeLE7I4H2u5U+sldmm0xsjkvthf6S79+AIdo7M+5AAAA" } } ] diff --git a/testdata/list-transactional-emails.replay.json b/testdata/list-transactional-emails.replay.json new file mode 100644 index 0000000..d2838e9 --- /dev/null +++ b/testdata/list-transactional-emails.replay.json @@ -0,0 +1,118 @@ +{ + "Initial": null, + "Version": "0.2", + "Converter": { + "ScrubBody": null, + "ClearHeaders": [ + "^X-Goog-.*Encryption-Key$" + ], + "RemoveRequestHeaders": [ + "^Authorization$", + "^Proxy-Authorization$", + "^Connection$", + "^Content-Type$", + "^Date$", + "^Host$", + "^Transfer-Encoding$", + "^Via$", + "^X-Forwarded-.*$", + "^X-Cloud-Trace-Context$", + "^X-Goog-Api-Client$", + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "RemoveResponseHeaders": [ + "^X-Google-.*$", + "^X-Gfe-.*$" + ], + "ClearParams": null, + "RemoveParams": null + }, + "Entries": [ + { + "ID": "cbd8c2e9c9662ded", + "Request": { + "Method": "GET", + "URL": "https://app.loops.so/api/v1/transactional", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "User-Agent": [ + "Go-http-client/1.1" + ] + }, + "MediaType": "application/json", + "BodyParts": [ + "" + ] + }, + "Response": { + "StatusCode": 200, + "Proto": "HTTP/1.1", + "ProtoMajor": 1, + "ProtoMinor": 1, + "Header": { + "Access-Control-Allow-Credentials": [ + "true" + ], + "Access-Control-Allow-Headers": [ + "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" + ], + "Access-Control-Allow-Methods": [ + "GET,OPTIONS,PATCH,DELETE,POST,PUT" + ], + "Access-Control-Allow-Origin": [ + "*" + ], + "Cf-Cache-Status": [ + "DYNAMIC" + ], + "Cf-Ray": [ + "9bb4477e1f615b07-VIE" + ], + "Content-Encoding": [ + "gzip" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Fri, 09 Jan 2026 13:28:42 GMT" + ], + "Etag": [ + "W/\"wbtmtoqglwam\"" + ], + "Permissions-Policy": [ + "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" + ], + "Referrer-Policy": [ + "strict-origin-when-cross-origin" + ], + "Server": [ + "cloudflare" + ], + "Strict-Transport-Security": [ + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-Ratelimit-Limit": [ + "10" + ], + "X-Ratelimit-Remaining": [ + "9" + ], + "X-Robots-Tag": [ + "none" + ] + }, + "Body": "H4sIAAAAAAAA/22OwW6DMBBEfwXtGSLbhEB9bH6gqpJWasRhCxtKa2xkm0KE+PcaIbU95LY7szP7ZuixaTX61miQM3jjUT2TG5R3IEUMlvxgNdX/tZ7sEzYUZhZviXUNHo9B0+SPg3XGgtSDUpuyna/7EkONHkFeZmhrkFB1qRbfn8PEWNXQja6Kxgd1EyOEKHYhBo8K9VfkLWqH1UqKKpgKnT/3oYzWGsHEPuE84cWJ72UqZJrtsuLwBtu/F7QtvqsV8rLVlkv8S5Bd86r1nLExHz6E2Yv0UEwN/yN4JVWZjqIkOhrtjKLo7Mi6exRZwngi8hNnUmSS8x1L2T2KcimXH18iEnB+AQAA" + } + } + ] +} \ No newline at end of file diff --git a/testdata/send-event.replay.json b/testdata/send-event.replay.json index 6504adb..317e033 100644 --- a/testdata/send-event.replay.json +++ b/testdata/send-event.replay.json @@ -30,7 +30,7 @@ }, "Entries": [ { - "ID": "08042e61b063f319", + "ID": "d4577a8f24072d0e", "Request": { "Method": "POST", "URL": "https://app.loops.so/api/v1/events/send", @@ -68,29 +68,23 @@ "Access-Control-Allow-Origin": [ "*" ], - "Cache-Control": [ - "public, max-age=0, must-revalidate" - ], "Cf-Cache-Status": [ "DYNAMIC" ], "Cf-Ray": [ - "8e48a9c63e85c268-VIE" - ], - "Content-Length": [ - "16" + "9bb447c52bb75a89-VIE" ], - "Content-Security-Policy": [ - "default-src 'self'; connect-src 'self' https://loops-prod-mjml-bucket.s3.amazonaws.com https://loops-prod-csv-uploads.s3.amazonaws.com https://vitals.vercel-insights.com https://browser-intake-datadoghq.com https://*.google-analytics.com https://api.zapier.com https://app.atlas.so https://app.getatlas.io wss://app.atlas.so https://*.filestackapi.com https://atlas-prod-uploads.s3.amazonaws.com https://ipgeolocation.abstractapi.com https://zapier.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.zapier.com https://www.googletagmanager.com https://app.getatlas.io https://app.atlas.so https://static.filestackapi.com https://files.atlas.so; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.zapier.com https://static.filestackapi.com; img-src 'self' blob: data: https://d3b9kr64nievew.cloudfront.net https://atlas-prod-uploads.s3.amazonaws.com https://files.getatlas.io https://files.atlas.so https://static.filestackapi.com https://cdn.filestackcontent.com https://zapier-images.imgix.net; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-src 'self' https://zapier.com; media-src 'self' data:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" + "Content-Encoding": [ + "gzip" ], "Content-Type": [ "application/json; charset=utf-8" ], "Date": [ - "Mon, 18 Nov 2024 14:31:17 GMT" + "Fri, 09 Jan 2026 13:28:53 GMT" ], "Etag": [ - "\"17a6zzdutk1g\"" + "W/\"17a6zzdutk1g\"" ], "Permissions-Policy": [ "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" @@ -102,28 +96,25 @@ "cloudflare" ], "Strict-Transport-Security": [ - "max-age=63072000" + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" ], "X-Content-Type-Options": [ "nosniff" ], - "X-Matched-Path": [ - "/api/v1/events/send" - ], "X-Ratelimit-Limit": [ "10" ], "X-Ratelimit-Remaining": [ "9" ], - "X-Vercel-Cache": [ - "MISS" - ], - "X-Vercel-Id": [ - "fra1::iad1::c627s-1731940276337-17adbbf856d6" + "X-Robots-Tag": [ + "none" ] }, - "Body": "eyJzdWNjZXNzIjp0cnVlfQ==" + "Body": "H4sIAAAAAAAA/6tWKi5NTk4tLlayKikqTa0FAN/39mYQAAAA" } } ] diff --git a/testdata/update-contact.replay.json b/testdata/update-contact.replay.json index 6656a5b..0c976b7 100644 --- a/testdata/update-contact.replay.json +++ b/testdata/update-contact.replay.json @@ -30,7 +30,7 @@ }, "Entries": [ { - "ID": "996c37fd1574a10b", + "ID": "6a36d052f1687fbc", "Request": { "Method": "PUT", "URL": "https://app.loops.so/api/v1/contacts/update", @@ -68,29 +68,23 @@ "Access-Control-Allow-Origin": [ "*" ], - "Cache-Control": [ - "public, max-age=0, must-revalidate" - ], "Cf-Cache-Status": [ "DYNAMIC" ], "Cf-Ray": [ - "8e48e0ae1cb75ae9-VIE" + "9bb421ff8f646cb4-VIE" ], "Content-Encoding": [ "gzip" ], - "Content-Security-Policy": [ - "default-src 'self'; connect-src 'self' https://loops-prod-mjml-bucket.s3.amazonaws.com https://loops-prod-csv-uploads.s3.amazonaws.com https://vitals.vercel-insights.com https://browser-intake-datadoghq.com https://*.google-analytics.com https://api.zapier.com https://app.atlas.so https://app.getatlas.io wss://app.atlas.so https://*.filestackapi.com https://atlas-prod-uploads.s3.amazonaws.com https://ipgeolocation.abstractapi.com https://zapier.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.zapier.com https://www.googletagmanager.com https://app.getatlas.io https://app.atlas.so https://static.filestackapi.com https://files.atlas.so; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.zapier.com https://static.filestackapi.com; img-src 'self' blob: data: https://d3b9kr64nievew.cloudfront.net https://atlas-prod-uploads.s3.amazonaws.com https://files.getatlas.io https://files.atlas.so https://static.filestackapi.com https://cdn.filestackcontent.com https://zapier-images.imgix.net; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-src 'self' https://zapier.com; media-src 'self' data:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" - ], "Content-Type": [ "application/json; charset=utf-8" ], "Date": [ - "Mon, 18 Nov 2024 15:08:45 GMT" + "Fri, 09 Jan 2026 13:03:06 GMT" ], "Etag": [ - "W/\"oai0fa27xa1d\"" + "W/\"v3jrx4hjv1d\"" ], "Permissions-Policy": [ "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" @@ -102,28 +96,25 @@ "cloudflare" ], "Strict-Transport-Security": [ - "max-age=63072000" + "max-age=31536000; includeSubDomains" + ], + "Vary": [ + "Accept-Encoding" ], "X-Content-Type-Options": [ "nosniff" ], - "X-Matched-Path": [ - "/api/v1/contacts/update" - ], "X-Ratelimit-Limit": [ "10" ], "X-Ratelimit-Remaining": [ "9" ], - "X-Vercel-Cache": [ - "MISS" - ], - "X-Vercel-Id": [ - "fra1::iad1::8jstk-1731942525246-b3b97f70f898" + "X-Robots-Tag": [ + "none" ] }, - "Body": "H4sIAAAAAAAAA6pWKi5NTk4tLlayKikqTdVRykxRslJKzjXOM8nOLE00MEo2KLEwtkwqqUzOK081VKoFAAAA//8DAKOElQ0xAAAA" + "Body": "H4sIAAAAAAAA/wExAM7/eyJzdWNjZXNzIjp0cnVlLCJpZCI6ImNtazZ2eXViMDBjN2IwaTA0ZGxyZWdlaXQifdgt8TAxAAAA" } } ] From be91dfe61362aaa706d7db0743d0d10b815c4aac Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Fri, 9 Jan 2026 15:03:35 +0100 Subject: [PATCH 2/2] fix lint issue --- examples/contact-crud/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/contact-crud/main.go b/examples/contact-crud/main.go index 9ff12de..fda4a18 100644 --- a/examples/contact-crud/main.go +++ b/examples/contact-crud/main.go @@ -26,7 +26,7 @@ func main() { LastName: loops.String("Armstrong"), Subscribed: true, // custom user defined properties for contacts - Properties: map[string]interface{}{ + Properties: map[string]any{ "role": "Astronaut", }, })