Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DE-1315 Add support for metrics API #328

Merged
merged 49 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
31fbc58
refactored
vtopc Sep 17, 2024
72ec3c8
refactored and deprecate
vtopc Sep 17, 2024
73d6a29
RFC2822Time
vtopc Sep 17, 2024
c3d2e09
WIP
vtopc Sep 17, 2024
f028688
merged master
vtopc Sep 17, 2024
8136d07
moved Resolution
vtopc Sep 17, 2024
9495404
bytes.Buffer
vtopc Sep 17, 2024
d4fe9a0
NOTE
vtopc Sep 17, 2024
66a47bb
mailgun-go
vtopc Sep 18, 2024
cd73665
check v1
vtopc Sep 18, 2024
b7456ba
TODO
vtopc Sep 18, 2024
907cd26
filter by domain
vtopc Sep 18, 2024
7a92b35
removed redundant
vtopc Sep 18, 2024
1b63574
fmt
vtopc Sep 18, 2024
e5fa39c
typo
vtopc Sep 18, 2024
bd98c8f
sort
vtopc Oct 19, 2024
4ded58f
added iterator
vtopc Oct 19, 2024
dbe1bc2
env
vtopc Oct 19, 2024
555f55e
allow v1
vtopc Oct 19, 2024
0903695
ExampleMailgunImpl_ListMetrics
vtopc Oct 19, 2024
c5e99db
ExampleMailgunImpl_ListMetrics
vtopc Oct 19, 2024
b401765
better error
vtopc Oct 19, 2024
a749b31
UTC
vtopc Oct 19, 2024
3beb189
switched RFC2822Time to RFC1123Z
vtopc Oct 19, 2024
584de81
end
vtopc Oct 19, 2024
4d131c2
omitempty
vtopc Oct 19, 2024
d864c01
for
vtopc Oct 19, 2024
8fef31f
removed TODO
vtopc Oct 19, 2024
4e85bfe
omitempty
vtopc Oct 19, 2024
697621d
Duration
vtopc Oct 19, 2024
57d4049
link to docs
vtopc Oct 19, 2024
92c47e7
Merge branch 'master' of github.com:mailgun/mailgun-go into vtopc/DE-…
vtopc Oct 19, 2024
7f45c96
get rid of github.com/pkg/errors again
vtopc Oct 19, 2024
6a9ff1a
fixed TestInvalidBaseAPI
vtopc Oct 19, 2024
b7b869f
fixed TestAddDelBounceList
vtopc Oct 19, 2024
196c144
renamed ListMetrics test
vtopc Oct 19, 2024
6f869a0
switched response to RFC2822Time
vtopc Oct 19, 2024
7648864
removed not used
vtopc Oct 19, 2024
4057f29
updated Mailgun interface
vtopc Oct 19, 2024
281cccc
Makefile
vtopc Oct 21, 2024
b4a15e8
switched to integration test
vtopc Oct 21, 2024
c1b05f1
get rid of "log" package
vtopc Oct 21, 2024
b9a428d
pre-alloc
vtopc Oct 24, 2024
c686e0c
added mock
vtopc Oct 24, 2024
24f0076
added unit test
vtopc Oct 25, 2024
e31765f
simplified
vtopc Oct 25, 2024
2edd829
nil check
vtopc Oct 25, 2024
d88c9b0
assert response
vtopc Oct 25, 2024
519cb67
sort
vtopc Oct 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
.idea/
cmd/mailgun/mailgun
/.env
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ $(NILAWAY):
go install go.uber.org/nilaway/cmd/nilaway@latest

.PHONY: all
all:
all: test

.PHONY: test
test:
export GO111MODULE=on; go test . -v

.PHONY: godoc
Expand Down
110 changes: 110 additions & 0 deletions analytics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package mailgun

import (
"context"
"strings"

"github.com/mailgun/errors"
)

type MetricsPagination struct {
// Colon-separated value indicating column name and sort direction e.g. 'domain:asc'.
Sort string `json:"sort"`
// The number of items to skip over when satisfying the request. To get the first page of data set skip to zero. Then increment the skip by the limit for subsequent calls.
Skip int `json:"skip"`
// The maximum number of items returned in the response.
Limit int `json:"limit"`
// The total number of items in the query result set.
Total int `json:"total"`
}

// ListMetrics returns domain/account metrics.
//
// NOTE: Only for v1 API. To use the /v1 version define MG_URL in the environment variable
// as `https://api.mailgun.net/v1` or set `mg.SetAPIBase("https://api.mailgun.net/v1")`
//
// https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/
func (mg *MailgunImpl) ListMetrics(opts MetricsOptions) (*MetricsIterator, error) {
if !strings.HasSuffix(mg.APIBase(), "/v1") {
return nil, errors.New("only v1 API is supported")
}

domain := mg.Domain()
if domain != "" {
domainFilter := MetricsFilterPredicate{
Attribute: "domain",
Comparator: "=",
LabeledValues: []MetricsLabeledValue{{Label: domain, Value: domain}},
}

opts.Filter.BoolGroupAnd = append(opts.Filter.BoolGroupAnd, domainFilter)
}

if opts.Pagination.Limit == 0 {
opts.Pagination.Limit = 10
}

req := newHTTPRequest(generatePublicApiUrl(mg, metricsEndpoint))
req.setClient(mg.Client())
req.setBasicAuth(basicAuthUser, mg.APIKey())

return &MetricsIterator{
opts: opts,
req: req,
}, nil
}

type MetricsIterator struct {
opts MetricsOptions
req *httpRequest
err error
}

func (iter *MetricsIterator) Err() error {
return iter.err
}

// Next retrieves the next page of items from the api. Returns false when there are
// no more pages to retrieve or if there was an error.
// Use `.Err()` to retrieve the error
func (iter *MetricsIterator) Next(ctx context.Context, resp *MetricsResponse) (more bool) {
if iter.err != nil {
return false
}

iter.err = iter.fetch(ctx, resp)
if iter.err != nil {
return false
}

iter.opts.Pagination.Skip = iter.opts.Pagination.Skip + iter.opts.Pagination.Limit

if len(resp.Items) < iter.opts.Pagination.Limit {
return false
}

return true
}

func (iter *MetricsIterator) fetch(ctx context.Context, resp *MetricsResponse) error {
if resp == nil {
return errors.New("resp cannot be nil")
}

payload := newJSONEncodedPayload(iter.opts)

httpResp, err := makePostRequest(ctx, iter.req, payload)
if err != nil {
return err
}

// preallocate
resp.Items = make([]MetricsItem, 0, iter.opts.Pagination.Limit)

err = httpResp.parseFromJSON(resp)
if err != nil {
return errors.Wrap(err, "decoding response")
}

return nil
}
40 changes: 40 additions & 0 deletions analytics_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package mailgun

type MetricsOptions struct {
// A start date (default: 7 days before current time).
Start RFC2822Time `json:"start,omitempty"`
// An end date (default: current time).
End RFC2822Time `json:"end,omitempty"`
// A resolution in the format of 'day' 'hour' 'month'. Default is day.
Resolution Resolution `json:"resolution,omitempty"`
// A duration in the format of '1d' '2h' '2m'.
// If duration is provided then it is calculated from the end date and overwrites the start date.
Duration string `json:"duration,omitempty"`
// Attributes of the metric data such as 'time' 'domain' 'ip' 'ip_pool' 'recipient_domain' 'tag' 'country' 'subaccount'.
Dimensions []string `json:"dimensions,omitempty"`
// Name of the metrics to receive the stats for such as 'accepted_count' 'delivered_count' 'accepted_rate'.
Metrics []string `json:"metrics,omitempty"`
// Filters to apply to the query.
Filter MetricsFilterPredicateGroup `json:"filter,omitempty"`
// Include stats from all subaccounts.
IncludeSubaccounts bool `json:"include_subaccounts,omitempty"`
// Include top-level aggregate metrics.
IncludeAggregates bool `json:"include_aggregates,omitempty"`
// Attributes used for pagination and sorting.
Pagination MetricsPagination `json:"pagination,omitempty"`
}

type MetricsLabeledValue struct {
Label string `json:"label"`
Value string `json:"value"`
}

type MetricsFilterPredicate struct {
Attribute string `json:"attribute"`
Comparator string `json:"comparator"`
LabeledValues []MetricsLabeledValue `json:"values,omitempty"`
}

type MetricsFilterPredicateGroup struct {
BoolGroupAnd []MetricsFilterPredicate `json:"AND,omitempty"`
}
95 changes: 95 additions & 0 deletions analytics_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package mailgun

type MetricsResponse struct {
Start RFC2822Time `json:"start"`
End RFC2822Time `json:"end"`
Resolution Resolution `json:"resolution"`
Duration string `json:"duration"`
Dimensions []string `json:"dimensions"`
Aggregates MetricsAggregates `json:"aggregates"`
Items []MetricsItem `json:"items"`
Pagination MetricsPagination `json:"pagination"`
}

type MetricsItem struct {
Dimensions []MetricsDimension `json:"dimensions"`
Metrics Metrics `json:"metrics"`
}

type MetricsAggregates struct {
Metrics Metrics `json:"metrics"`
}

type Metrics struct {
AcceptedIncomingCount *uint64 `json:"accepted_incoming_count,omitempty"`
AcceptedOutgoingCount *uint64 `json:"accepted_outgoing_count,omitempty"`
AcceptedCount *uint64 `json:"accepted_count,omitempty"`
DeliveredSMTPCount *uint64 `json:"delivered_smtp_count,omitempty"`
DeliveredHTTPCount *uint64 `json:"delivered_http_count,omitempty"`
DeliveredOptimizedCount *uint64 `json:"delivered_optimized_count,omitempty"`
DeliveredCount *uint64 `json:"delivered_count,omitempty"`
StoredCount *uint64 `json:"stored_count,omitempty"`
ProcessedCount *uint64 `json:"processed_count,omitempty"`
SentCount *uint64 `json:"sent_count,omitempty"`
OpenedCount *uint64 `json:"opened_count,omitempty"`
ClickedCount *uint64 `json:"clicked_count,omitempty"`
UniqueOpenedCount *uint64 `json:"unique_opened_count,omitempty"`
UniqueClickedCount *uint64 `json:"unique_clicked_count,omitempty"`
UnsubscribedCount *uint64 `json:"unsubscribed_count,omitempty"`
ComplainedCount *uint64 `json:"complained_count,omitempty"`
FailedCount *uint64 `json:"failed_count,omitempty"`
TemporaryFailedCount *uint64 `json:"temporary_failed_count,omitempty"`
PermanentFailedCount *uint64 `json:"permanent_failed_count,omitempty"`
ESPBlockCount *uint64 `json:"esp_block_count,omitempty"`
WebhookCount *uint64 `json:"webhook_count,omitempty"`
PermanentFailedOptimizedCount *uint64 `json:"permanent_failed_optimized_count,omitempty"`
PermanentFailedOldCount *uint64 `json:"permanent_failed_old_count,omitempty"`
BouncedCount *uint64 `json:"bounced_count,omitempty"`
HardBouncesCount *uint64 `json:"hard_bounces_count,omitempty"`
SoftBouncesCount *uint64 `json:"soft_bounces_count,omitempty"`
DelayedBounceCount *uint64 `json:"delayed_bounce_count,omitempty"`
SuppressedBouncesCount *uint64 `json:"suppressed_bounces_count,omitempty"`
SuppressedUnsubscribedCount *uint64 `json:"suppressed_unsubscribed_count,omitempty"`
SuppressedComplaintsCount *uint64 `json:"suppressed_complaints_count,omitempty"`
DeliveredFirstAttemptCount *uint64 `json:"delivered_first_attempt_count,omitempty"`
DelayedFirstAttemptCount *uint64 `json:"delayed_first_attempt_count,omitempty"`
DeliveredSubsequentCount *uint64 `json:"delivered_subsequent_count,omitempty"`
DeliveredTwoPlusAttemptsCount *uint64 `json:"delivered_two_plus_attempts_count,omitempty"`

DeliveredRate string `json:"delivered_rate,omitempty"`
OpenedRate string `json:"opened_rate,omitempty"`
ClickedRate string `json:"clicked_rate,omitempty"`
UniqueOpenedRate string `json:"unique_opened_rate,omitempty"`
UniqueClickedRate string `json:"unique_clicked_rate,omitempty"`
UnsubscribedRate string `json:"unsubscribed_rate,omitempty"`
ComplainedRate string `json:"complained_rate,omitempty"`
BounceRate string `json:"bounce_rate,omitempty"`
FailRate string `json:"fail_rate,omitempty"`
PermanentFailRate string `json:"permanent_fail_rate,omitempty"`
TemporaryFailRate string `json:"temporary_fail_rate,omitempty"`
DelayedRate string `json:"delayed_rate,omitempty"`

// usage metrics
EmailValidationCount *uint64 `json:"email_validation_count,omitempty"`
EmailValidationPublicCount *uint64 `json:"email_validation_public_count,omitempty"`
EmailValidationValidCount *uint64 `json:"email_validation_valid_count,omitempty"`
EmailValidationSingleCount *uint64 `json:"email_validation_single_count,omitempty"`
EmailValidationBulkCount *uint64 `json:"email_validation_bulk_count,omitempty"`
EmailValidationListCount *uint64 `json:"email_validation_list_count,omitempty"`
EmailValidationMailgunCount *uint64 `json:"email_validation_mailgun_count,omitempty"`
EmailValidationMailjetCount *uint64 `json:"email_validation_mailjet_count,omitempty"`
EmailPreviewCount *uint64 `json:"email_preview_count,omitempty"`
EmailPreviewFailedCount *uint64 `json:"email_preview_failed_count,omitempty"`
LinkValidationCount *uint64 `json:"link_validation_count,omitempty"`
LinkValidationFailedCount *uint64 `json:"link_validation_failed_count,omitempty"`
SeedTestCount *uint64 `json:"seed_test_count,omitempty"`
}

type MetricsDimension struct {
// The dimension
Dimension string `json:"dimension"`
// The dimension value
Value string `json:"value"`
// The dimension value in displayable form
DisplayValue string `json:"display_value"`
}
65 changes: 65 additions & 0 deletions analytics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package mailgun_test

import (
"context"
"testing"

"github.com/mailgun/mailgun-go/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestListMetrics(t *testing.T) {
mg := mailgun.NewMailgun(testDomain, testKey)
mg.SetAPIBase(server.URL1())

start, _ := mailgun.NewRFC2822Time("Tue, 24 Sep 2024 00:00:00 +0000")
end, _ := mailgun.NewRFC2822Time("Tue, 24 Oct 2024 00:00:00 +0000")

opts := mailgun.MetricsOptions{
Start: start,
End: end,
Pagination: mailgun.MetricsPagination{
Limit: 10,
},
}

wantResp := mailgun.MetricsResponse{
Start: start,
End: end,
Resolution: "day",
Duration: "30d",
Dimensions: []string{"time"},
Items: []mailgun.MetricsItem{
{
Dimensions: []mailgun.MetricsDimension{{
Dimension: "time",
Value: "Tue, 24 Sep 2024 00:00:00 +0000",
DisplayValue: "Tue, 24 Sep 2024 00:00:00 +0000",
}},
Metrics: mailgun.Metrics{
SentCount: ptr(uint64(4)),
DeliveredCount: ptr(uint64(3)),
OpenedCount: ptr(uint64(2)),
FailedCount: ptr(uint64(1)),
},
},
},
Pagination: mailgun.MetricsPagination{
Sort: "",
Skip: 0,
Limit: 10,
Total: 1,
},
}

it, err := mg.ListMetrics(opts)
require.NoError(t, err)

var page mailgun.MetricsResponse
ctx := context.Background()
more := it.Next(ctx, &page)
require.Nil(t, it.Err())
assert.False(t, more)
assert.Equal(t, wantResp, page)
}
4 changes: 2 additions & 2 deletions bounces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func TestAddDelBounceList(t *testing.T) {
return false
}

createdAt, err := mailgun.NewRFC2822Time("Thu, 13 Oct 2011 18:02:00 UTC")
createdAt, err := mailgun.NewRFC2822Time("Thu, 13 Oct 2011 18:02:00 +0000")
if err != nil {
t.Fatalf("invalid time")
}
Expand Down Expand Up @@ -162,7 +162,7 @@ func TestAddDelBounceList(t *testing.T) {
t.Fatalf("Expected at least one bounce for %s", expect.Address)
}
t.Logf("Bounce Created At: %s", bounce.CreatedAt)
if !expect.CreatedAt.IsZero() && bounce.CreatedAt != expect.CreatedAt {
if !expect.CreatedAt.IsZero() && !time.Time(bounce.CreatedAt).Equal(time.Time(expect.CreatedAt)) {
t.Fatalf("Expected bounce createdAt to be %s, got %s", expect.CreatedAt, bounce.CreatedAt)
}
}
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/go-chi/chi/v5 v5.0.8
github.com/json-iterator/go v1.1.10
github.com/mailgun/errors v0.3.0
github.com/stretchr/testify v1.9.0
)

require (
Expand All @@ -15,6 +16,8 @@ require (
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
5 changes: 3 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
4 changes: 2 additions & 2 deletions httphelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/mailgun/errors"
)

var validURL = regexp.MustCompile(`/v[2-5].*`)
var validURL = regexp.MustCompile(`/v[1-5].*`)

type httpRequest struct {
URL string
Expand Down Expand Up @@ -332,7 +332,7 @@ func (r *httpRequest) generateUrlWithParameters() (string, error) {
}

if !validURL.MatchString(url.Path) {
return "", errors.New(`BaseAPI must end with a /v2, /v3 or /v4; setBaseAPI("https://host/v3")`)
return "", errors.New(`BaseAPI must end with a /v1, /v2, /v3 or /v4; setBaseAPI("https://host/v3")`)
}

q := url.Query()
Expand Down
Loading
Loading