From 6c23fb3173bbe496059182a61ce98b5e0152505d Mon Sep 17 00:00:00 2001 From: Cameron Sparr Date: Tue, 1 Dec 2015 14:05:24 -0700 Subject: [PATCH] Mailchimp report plugin --- CHANGELOG.md | 3 + README.md | 1 + agent.go | 4 + plugins/all/all.go | 1 + plugins/apache/apache_test.go | 2 +- plugins/mailchimp/chimp_api.go | 234 ++++++++ plugins/mailchimp/mailchimp.go | 113 ++++ plugins/mailchimp/mailchimp_test.go | 832 ++++++++++++++++++++++++++++ 8 files changed, 1189 insertions(+), 1 deletion(-) create mode 100644 plugins/mailchimp/chimp_api.go create mode 100644 plugins/mailchimp/mailchimp.go create mode 100644 plugins/mailchimp/mailchimp_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4e90755..4f90c585d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,12 @@ - [#410](https://github.com/influxdb/telegraf/pull/410): Additional redis metrics. Thanks @vlaadbrain! - [#414](https://github.com/influxdb/telegraf/issues/414): Jolokia plugin auth parameters - [#415](https://github.com/influxdb/telegraf/issues/415): memcached plugin: support unix sockets +- [#418](https://github.com/influxdb/telegraf/pull/418): memcached plugin additional unit tests. +- [#408](https://github.com/influxdb/telegraf/pull/408): MailChimp plugin. ### Bugfixes - [#405](https://github.com/influxdb/telegraf/issues/405): Prometheus output cardinality issue +- [#388](https://github.com/influxdb/telegraf/issues/388): Fix collection hangup when cpu times decrement. ## v0.2.3 [2015-11-30] diff --git a/README.md b/README.md index cd2da2dba..e6cf28e25 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ Telegraf currently has support for collecting metrics from: * jolokia (remote JMX with JSON over HTTP) * leofs * lustre2 +* mailchimp * memcached * mongodb * mysql diff --git a/agent.go b/agent.go index e1292bffe..4bc001182 100644 --- a/agent.go +++ b/agent.go @@ -114,6 +114,10 @@ func (a *Agent) gatherParallel(pointChan chan *client.Point) error { }(plugin) } + if counter == 0 { + return nil + } + wg.Wait() elapsed := time.Since(start) diff --git a/plugins/all/all.go b/plugins/all/all.go index ba0859f65..676d25707 100644 --- a/plugins/all/all.go +++ b/plugins/all/all.go @@ -13,6 +13,7 @@ import ( _ "github.com/influxdb/telegraf/plugins/kafka_consumer" _ "github.com/influxdb/telegraf/plugins/leofs" _ "github.com/influxdb/telegraf/plugins/lustre2" + _ "github.com/influxdb/telegraf/plugins/mailchimp" _ "github.com/influxdb/telegraf/plugins/memcached" _ "github.com/influxdb/telegraf/plugins/mongodb" _ "github.com/influxdb/telegraf/plugins/mysql" diff --git a/plugins/apache/apache_test.go b/plugins/apache/apache_test.go index 018542884..9688302ac 100644 --- a/plugins/apache/apache_test.go +++ b/plugins/apache/apache_test.go @@ -29,7 +29,7 @@ ConnsAsyncClosing: 205 Scoreboard: WW_____W_RW_R_W__RRR____WR_W___WW________W_WW_W_____R__R_WR__WRWR_RRRW___R_RWW__WWWRW__R_RW___RR_RW_R__W__WR_WWW______WWR__R___R_WR_W___RW______RR________________W______R__RR______W________________R____R__________________________RW_W____R_____W_R_________________R____RR__W___R_R____RW______R____W______W_W_R_R______R__R_R__________R____W_______WW____W____RR__W_____W_R_______W__________W___W____________W_______WRR_R_W____W_____R____W_WW_R____RRW__W............................................................................................................................................................................................................................................................................................................WRRWR____WR__RR_R___RWR_________W_R____RWRRR____R_R__RW_R___WWW_RW__WR_RRR____W___R____WW_R__R___RR_W_W_RRRRWR__RRWR__RRW_W_RRRW_R_RR_W__RR_RWRR_R__R___RR_RR______R__RR____R_____W_R_R_R__R__R__________W____WW_R___R_R___R_________RR__RR____RWWWW___W_R________R_R____R_W___W___R___W_WRRWW_______R__W_RW_______R________RR__R________W_______________________W_W______________RW_________WR__R___R__R_______________WR_R_________W___RW_____R____________W____...................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... ` -func TestHTTPInflux(t *testing.T) { +func TestHTTPApache(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintln(w, apacheStatus) diff --git a/plugins/mailchimp/chimp_api.go b/plugins/mailchimp/chimp_api.go new file mode 100644 index 000000000..fe2c56d0c --- /dev/null +++ b/plugins/mailchimp/chimp_api.go @@ -0,0 +1,234 @@ +package mailchimp + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "regexp" + "sync" +) + +const ( + reports_endpoint string = "/3.0/reports" + reports_endpoint_campaign string = "/3.0/reports/%s" +) + +var mailchimp_datacenter = regexp.MustCompile("[a-z]+[0-9]+$") + +type ChimpAPI struct { + Transport http.RoundTripper + Debug bool + + sync.Mutex + + url *url.URL +} + +type ReportsParams struct { + Count string + Offset string + SinceSendTime string + BeforeSendTime string +} + +func (p *ReportsParams) String() string { + v := url.Values{} + if p.Count != "" { + v.Set("count", p.Count) + } + if p.Offset != "" { + v.Set("offset", p.Offset) + } + if p.BeforeSendTime != "" { + v.Set("before_send_time", p.BeforeSendTime) + } + if p.SinceSendTime != "" { + v.Set("since_send_time", p.SinceSendTime) + } + return v.Encode() +} + +func NewChimpAPI(apiKey string) *ChimpAPI { + u := &url.URL{} + u.Scheme = "https" + u.Host = fmt.Sprintf("%s.api.mailchimp.com", mailchimp_datacenter.FindString(apiKey)) + u.User = url.UserPassword("", apiKey) + return &ChimpAPI{url: u} +} + +type APIError struct { + Status int `json:"status"` + Type string `json:"type"` + Title string `json:"title"` + Detail string `json:"detail"` + Instance string `json:"instance"` +} + +func (e APIError) Error() string { + return fmt.Sprintf("ERROR %v: %v. See %v", e.Status, e.Title, e.Type) +} + +func chimpErrorCheck(body []byte) error { + var e APIError + json.Unmarshal(body, &e) + if e.Title != "" || e.Status != 0 { + return e + } + return nil +} + +func (a *ChimpAPI) GetReports(params ReportsParams) (ReportsResponse, error) { + a.Lock() + defer a.Unlock() + a.url.Path = reports_endpoint + + var response ReportsResponse + rawjson, err := runChimp(a, params) + if err != nil { + return response, err + } + + err = json.Unmarshal(rawjson, &response) + if err != nil { + return response, err + } + + return response, nil +} + +func (a *ChimpAPI) GetReport(campaignID string) (Report, error) { + a.Lock() + defer a.Unlock() + a.url.Path = fmt.Sprintf(reports_endpoint_campaign, campaignID) + + var response Report + rawjson, err := runChimp(a, ReportsParams{}) + if err != nil { + return response, err + } + + err = json.Unmarshal(rawjson, &response) + if err != nil { + return response, err + } + + return response, nil +} + +func runChimp(api *ChimpAPI, params ReportsParams) ([]byte, error) { + client := &http.Client{Transport: api.Transport} + + var b bytes.Buffer + req, err := http.NewRequest("GET", api.url.String(), &b) + if err != nil { + return nil, err + } + req.URL.RawQuery = params.String() + req.Header.Set("User-Agent", "Telegraf-MailChimp-Plugin") + if api.Debug { + log.Printf("Request URL: %s", req.URL.String()) + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if api.Debug { + log.Printf("Response Body:%s", string(body)) + } + + if err = chimpErrorCheck(body); err != nil { + return nil, err + } + return body, nil +} + +type ReportsResponse struct { + Reports []Report `json:"reports"` + TotalItems int `json:"total_items"` +} + +type Report struct { + ID string `json:"id"` + CampaignTitle string `json:"campaign_title"` + Type string `json:"type"` + EmailsSent int `json:"emails_sent"` + AbuseReports int `json:"abuse_reports"` + Unsubscribed int `json:"unsubscribed"` + SendTime string `json:"send_time"` + + TimeSeries []TimeSerie + Bounces Bounces `json:"bounces"` + Forwards Forwards `json:"forwards"` + Opens Opens `json:"opens"` + Clicks Clicks `json:"clicks"` + FacebookLikes FacebookLikes `json:"facebook_likes"` + IndustryStats IndustryStats `json:"industry_stats"` + ListStats ListStats `json:"list_stats"` +} + +type Bounces struct { + HardBounces int `json:"hard_bounces"` + SoftBounces int `json:"soft_bounces"` + SyntaxErrors int `json:"syntax_errors"` +} + +type Forwards struct { + ForwardsCount int `json:"forwards_count"` + ForwardsOpens int `json:"forwards_opens"` +} + +type Opens struct { + OpensTotal int `json:"opens_total"` + UniqueOpens int `json:"unique_opens"` + OpenRate float64 `json:"open_rate"` + LastOpen string `json:"last_open"` +} + +type Clicks struct { + ClicksTotal int `json:"clicks_total"` + UniqueClicks int `json:"unique_clicks"` + UniqueSubscriberClicks int `json:"unique_subscriber_clicks"` + ClickRate float64 `json:"click_rate"` + LastClick string `json:"last_click"` +} + +type FacebookLikes struct { + RecipientLikes int `json:"recipient_likes"` + UniqueLikes int `json:"unique_likes"` + FacebookLikes int `json:"facebook_likes"` +} + +type IndustryStats struct { + Type string `json:"type"` + OpenRate float64 `json:"open_rate"` + ClickRate float64 `json:"click_rate"` + BounceRate float64 `json:"bounce_rate"` + UnopenRate float64 `json:"unopen_rate"` + UnsubRate float64 `json:"unsub_rate"` + AbuseRate float64 `json:"abuse_rate"` +} + +type ListStats struct { + SubRate float64 `json:"sub_rate"` + UnsubRate float64 `json:"unsub_rate"` + OpenRate float64 `json:"open_rate"` + ClickRate float64 `json:"click_rate"` +} + +type TimeSerie struct { + TimeStamp string `json:"timestamp"` + EmailsSent int `json:"emails_sent"` + UniqueOpens int `json:"unique_opens"` + RecipientsClick int `json:"recipients_click"` +} diff --git a/plugins/mailchimp/mailchimp.go b/plugins/mailchimp/mailchimp.go new file mode 100644 index 000000000..c1c7027f0 --- /dev/null +++ b/plugins/mailchimp/mailchimp.go @@ -0,0 +1,113 @@ +package mailchimp + +import ( + "fmt" + "time" + + "github.com/influxdb/telegraf/plugins" +) + +type MailChimp struct { + api *ChimpAPI + + ApiKey string + DaysOld int + CampaignId string +} + +var sampleConfig = ` + # MailChimp API key + # get from https://admin.mailchimp.com/account/api/ + api_key = "" # required + # Reports for campaigns sent more than days_old ago will not be collected. + # 0 means collect all. + days_old = 0 + # Campaign ID to get, if empty gets all campaigns, this option overrides days_old + # campaign_id = "" +` + +func (m *MailChimp) SampleConfig() string { + return sampleConfig +} + +func (m *MailChimp) Description() string { + return "Gathers metrics from the /3.0/reports MailChimp API" +} + +func (m *MailChimp) Gather(acc plugins.Accumulator) error { + if m.api == nil { + m.api = NewChimpAPI(m.ApiKey) + } + m.api.Debug = false + + if m.CampaignId == "" { + since := "" + if m.DaysOld > 0 { + now := time.Now() + d, _ := time.ParseDuration(fmt.Sprintf("%dh", 24*m.DaysOld)) + since = now.Add(-d).Format(time.RFC3339) + } + + reports, err := m.api.GetReports(ReportsParams{ + SinceSendTime: since, + }) + if err != nil { + return err + } + now := time.Now() + + for _, report := range reports.Reports { + gatherReport(acc, report, now) + } + } else { + report, err := m.api.GetReport(m.CampaignId) + if err != nil { + return err + } + now := time.Now() + gatherReport(acc, report, now) + } + + return nil +} + +func gatherReport(acc plugins.Accumulator, report Report, now time.Time) { + tags := make(map[string]string) + tags["id"] = report.ID + tags["campaign_title"] = report.CampaignTitle + acc.Add("emails_sent", report.EmailsSent, tags, now) + acc.Add("abuse_reports", report.AbuseReports, tags, now) + acc.Add("unsubscribed", report.Unsubscribed, tags, now) + acc.Add("hard_bounces", report.Bounces.HardBounces, tags, now) + acc.Add("soft_bounces", report.Bounces.SoftBounces, tags, now) + acc.Add("syntax_errors", report.Bounces.SyntaxErrors, tags, now) + acc.Add("forwards_count", report.Forwards.ForwardsCount, tags, now) + acc.Add("forwards_opens", report.Forwards.ForwardsOpens, tags, now) + acc.Add("opens_total", report.Opens.OpensTotal, tags, now) + acc.Add("unique_opens", report.Opens.UniqueOpens, tags, now) + acc.Add("open_rate", report.Opens.OpenRate, tags, now) + acc.Add("clicks_total", report.Clicks.ClicksTotal, tags, now) + acc.Add("unique_clicks", report.Clicks.UniqueClicks, tags, now) + acc.Add("unique_subscriber_clicks", report.Clicks.UniqueSubscriberClicks, tags, now) + acc.Add("click_rate", report.Clicks.ClickRate, tags, now) + acc.Add("facebook_recipient_likes", report.FacebookLikes.RecipientLikes, tags, now) + acc.Add("facebook_unique_likes", report.FacebookLikes.UniqueLikes, tags, now) + acc.Add("facebook_likes", report.FacebookLikes.FacebookLikes, tags, now) + acc.Add("industry_type", report.IndustryStats.Type, tags, now) + acc.Add("industry_open_rate", report.IndustryStats.OpenRate, tags, now) + acc.Add("industry_click_rate", report.IndustryStats.ClickRate, tags, now) + acc.Add("industry_bounce_rate", report.IndustryStats.BounceRate, tags, now) + acc.Add("industry_unopen_rate", report.IndustryStats.UnopenRate, tags, now) + acc.Add("industry_unsub_rate", report.IndustryStats.UnsubRate, tags, now) + acc.Add("industry_abuse_rate", report.IndustryStats.AbuseRate, tags, now) + acc.Add("list_stats_sub_rate", report.ListStats.SubRate, tags, now) + acc.Add("list_stats_unsub_rate", report.ListStats.UnsubRate, tags, now) + acc.Add("list_stats_open_rate", report.ListStats.OpenRate, tags, now) + acc.Add("list_stats_click_rate", report.ListStats.ClickRate, tags, now) +} + +func init() { + plugins.Add("mailchimp", func() plugins.Plugin { + return &MailChimp{} + }) +} diff --git a/plugins/mailchimp/mailchimp_test.go b/plugins/mailchimp/mailchimp_test.go new file mode 100644 index 000000000..bd800f656 --- /dev/null +++ b/plugins/mailchimp/mailchimp_test.go @@ -0,0 +1,832 @@ +package mailchimp + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/influxdb/telegraf/testutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMailChimpGatherReports(t *testing.T) { + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, sampleReports) + }, + )) + defer ts.Close() + + u, err := url.ParseRequestURI(ts.URL) + require.NoError(t, err) + + api := &ChimpAPI{ + url: u, + Debug: true, + } + m := MailChimp{ + api: api, + } + + var acc testutil.Accumulator + err = m.Gather(&acc) + require.NoError(t, err) + + tags := make(map[string]string) + tags["id"] = "42694e9e57" + tags["campaign_title"] = "Freddie's Jokes Vol. 1" + + testInts := []struct { + measurement string + value int + }{ + {"emails_sent", 200}, + {"abuse_reports", 0}, + {"unsubscribed", 2}, + {"hard_bounces", 0}, + {"soft_bounces", 2}, + {"syntax_errors", 0}, + {"forwards_count", 0}, + {"forwards_opens", 0}, + {"opens_total", 186}, + {"unique_opens", 100}, + {"clicks_total", 42}, + {"unique_clicks", 400}, + {"unique_subscriber_clicks", 42}, + {"facebook_recipient_likes", 5}, + {"facebook_unique_likes", 8}, + {"facebook_likes", 42}, + } + for _, test := range testInts { + assert.True(t, acc.CheckTaggedValue(test.measurement, test.value, tags), + fmt.Sprintf("Measurement: %v, value: %v, tags: %v not found", + test.measurement, test.value, tags)) + } + + testFloats := []struct { + measurement string + value float64 + }{ + {"open_rate", 42}, + {"click_rate", 42}, + {"industry_open_rate", 0.17076777144396}, + {"industry_click_rate", 0.027431311866951}, + {"industry_bounce_rate", 0.0063767751251474}, + {"industry_unopen_rate", 0.82285545343089}, + {"industry_unsub_rate", 0.001436957032815}, + {"industry_abuse_rate", 0.00021111996110887}, + {"list_stats_sub_rate", 10}, + {"list_stats_unsub_rate", 20}, + {"list_stats_open_rate", 42}, + {"list_stats_click_rate", 42}, + } + for _, test := range testFloats { + assert.True(t, acc.CheckTaggedValue(test.measurement, test.value, tags), + fmt.Sprintf("Measurement: %v, value: %v, tags: %v not found", + test.measurement, test.value, tags)) + } + + testStrings := []struct { + measurement string + value string + }{ + {"industry_type", "Social Networks and Online Communities"}, + } + for _, test := range testStrings { + assert.True(t, acc.CheckTaggedValue(test.measurement, test.value, tags), + fmt.Sprintf("Measurement: %v, value: %v, tags: %v not found", + test.measurement, test.value, tags)) + } +} + +func TestMailChimpGatherReport(t *testing.T) { + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, sampleReport) + }, + )) + defer ts.Close() + + u, err := url.ParseRequestURI(ts.URL) + require.NoError(t, err) + + api := &ChimpAPI{ + url: u, + Debug: true, + } + m := MailChimp{ + api: api, + CampaignId: "test", + } + + var acc testutil.Accumulator + err = m.Gather(&acc) + require.NoError(t, err) + + tags := make(map[string]string) + tags["id"] = "42694e9e57" + tags["campaign_title"] = "Freddie's Jokes Vol. 1" + + testInts := []struct { + measurement string + value int + }{ + {"emails_sent", 200}, + {"abuse_reports", 0}, + {"unsubscribed", 2}, + {"hard_bounces", 0}, + {"soft_bounces", 2}, + {"syntax_errors", 0}, + {"forwards_count", 0}, + {"forwards_opens", 0}, + {"opens_total", 186}, + {"unique_opens", 100}, + {"clicks_total", 42}, + {"unique_clicks", 400}, + {"unique_subscriber_clicks", 42}, + {"facebook_recipient_likes", 5}, + {"facebook_unique_likes", 8}, + {"facebook_likes", 42}, + } + for _, test := range testInts { + assert.True(t, acc.CheckTaggedValue(test.measurement, test.value, tags), + fmt.Sprintf("Measurement: %v, value: %v, tags: %v not found", + test.measurement, test.value, tags)) + } + + testFloats := []struct { + measurement string + value float64 + }{ + {"open_rate", 42}, + {"click_rate", 42}, + {"industry_open_rate", 0.17076777144396}, + {"industry_click_rate", 0.027431311866951}, + {"industry_bounce_rate", 0.0063767751251474}, + {"industry_unopen_rate", 0.82285545343089}, + {"industry_unsub_rate", 0.001436957032815}, + {"industry_abuse_rate", 0.00021111996110887}, + {"list_stats_sub_rate", 10}, + {"list_stats_unsub_rate", 20}, + {"list_stats_open_rate", 42}, + {"list_stats_click_rate", 42}, + } + for _, test := range testFloats { + assert.True(t, acc.CheckTaggedValue(test.measurement, test.value, tags), + fmt.Sprintf("Measurement: %v, value: %v, tags: %v not found", + test.measurement, test.value, tags)) + } + + testStrings := []struct { + measurement string + value string + }{ + {"industry_type", "Social Networks and Online Communities"}, + } + for _, test := range testStrings { + assert.True(t, acc.CheckTaggedValue(test.measurement, test.value, tags), + fmt.Sprintf("Measurement: %v, value: %v, tags: %v not found", + test.measurement, test.value, tags)) + } +} + +func TestMailChimpGatherError(t *testing.T) { + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, sampleError) + }, + )) + defer ts.Close() + + u, err := url.ParseRequestURI(ts.URL) + require.NoError(t, err) + + api := &ChimpAPI{ + url: u, + Debug: true, + } + m := MailChimp{ + api: api, + CampaignId: "test", + } + + var acc testutil.Accumulator + err = m.Gather(&acc) + require.Error(t, err) +} + +var sampleReports = ` +{ + "reports": [ + { + "id": "42694e9e57", + "campaign_title": "Freddie's Jokes Vol. 1", + "type": "regular", + "emails_sent": 200, + "abuse_reports": 0, + "unsubscribed": 2, + "send_time": "2015-09-15T19:05:51+00:00", + "bounces": { + "hard_bounces": 0, + "soft_bounces": 2, + "syntax_errors": 0 + }, + "forwards": { + "forwards_count": 0, + "forwards_opens": 0 + }, + "opens": { + "opens_total": 186, + "unique_opens": 100, + "open_rate": 42, + "last_open": "2015-09-15T19:15:47+00:00" + }, + "clicks": { + "clicks_total": 42, + "unique_clicks": 400, + "unique_subscriber_clicks": 42, + "click_rate": 42, + "last_click": "2015-09-15T19:15:47+00:00" + }, + "facebook_likes": { + "recipient_likes": 5, + "unique_likes": 8, + "facebook_likes": 42 + }, + "industry_stats": { + "type": "Social Networks and Online Communities", + "open_rate": 0.17076777144396, + "click_rate": 0.027431311866951, + "bounce_rate": 0.0063767751251474, + "unopen_rate": 0.82285545343089, + "unsub_rate": 0.001436957032815, + "abuse_rate": 0.00021111996110887 + }, + "list_stats": { + "sub_rate": 10, + "unsub_rate": 20, + "open_rate": 42, + "click_rate": 42 + }, + "timeseries": [ + { + "timestamp": "2015-09-15T19:00:00+00:00", + "emails_sent": 198, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-15T20:00:00+00:00", + "emails_sent": 2, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-15T21:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-15T22:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-15T23:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T00:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T01:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T02:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T03:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T04:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T05:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T06:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T07:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T08:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T09:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T10:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T11:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T12:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T13:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T14:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T15:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T16:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T17:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T18:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + } + ], + "share_report": { + "share_url": "http://usX.vip-reports.net/reports/summary?u=xxxx&id=xxxx", + "share_password": "freddielikesjokes" + }, + "delivery_status": { + "enabled": false + }, + "_links": [ + { + "rel": "parent", + "href": "https://usX.api.mailchimp.com/3.0/reports", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Collection.json", + "schema": "https://api.mailchimp.com/schema/3.0/CollectionLinks/Reports.json" + }, + { + "rel": "self", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Instance.json" + }, + { + "rel": "campaign", + "href": "https://usX.api.mailchimp.com/3.0/campaigns/42694e9e57", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Campaigns/Instance.json" + }, + { + "rel": "sub-reports", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/sub-reports", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Sub/Collection.json" + }, + { + "rel": "abuse-reports", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/abuse-reports", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Abuse/Collection.json" + }, + { + "rel": "advice", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/advice", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Advice/Collection.json" + }, + { + "rel": "click-details", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/click-details", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/ClickDetails/Collection.json" + }, + { + "rel": "domain-performance", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/domain-performance", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/DomainPerformance/Collection.json" + }, + { + "rel": "eepurl", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/eepurl", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Eepurl/Collection.json" + }, + { + "rel": "email-activity", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/email-activity", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/EmailActivity/Collection.json" + }, + { + "rel": "locations", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/locations", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Locations/Collection.json" + }, + { + "rel": "sent-to", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/sent-to", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/SentTo/Collection.json" + }, + { + "rel": "unsubscribed", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/unsubscribed", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Unsubs/Collection.json" + } + ] + } + ], + "_links": [ + { + "rel": "parent", + "href": "https://usX.api.mailchimp.com/3.0/", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Root.json" + }, + { + "rel": "self", + "href": "https://usX.api.mailchimp.com/3.0/reports", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Collection.json", + "schema": "https://api.mailchimp.com/schema/3.0/CollectionLinks/Reports.json" + } + ], + "total_items": 1 +} +` + +var sampleReport = ` +{ + "id": "42694e9e57", + "campaign_title": "Freddie's Jokes Vol. 1", + "type": "regular", + "emails_sent": 200, + "abuse_reports": 0, + "unsubscribed": 2, + "send_time": "2015-09-15T19:05:51+00:00", + "bounces": { + "hard_bounces": 0, + "soft_bounces": 2, + "syntax_errors": 0 + }, + "forwards": { + "forwards_count": 0, + "forwards_opens": 0 + }, + "opens": { + "opens_total": 186, + "unique_opens": 100, + "open_rate": 42, + "last_open": "2015-09-15T19:15:47+00:00" + }, + "clicks": { + "clicks_total": 42, + "unique_clicks": 400, + "unique_subscriber_clicks": 42, + "click_rate": 42, + "last_click": "2015-09-15T19:15:47+00:00" + }, + "facebook_likes": { + "recipient_likes": 5, + "unique_likes": 8, + "facebook_likes": 42 + }, + "industry_stats": { + "type": "Social Networks and Online Communities", + "open_rate": 0.17076777144396, + "click_rate": 0.027431311866951, + "bounce_rate": 0.0063767751251474, + "unopen_rate": 0.82285545343089, + "unsub_rate": 0.001436957032815, + "abuse_rate": 0.00021111996110887 + }, + "list_stats": { + "sub_rate": 10, + "unsub_rate": 20, + "open_rate": 42, + "click_rate": 42 + }, + "timeseries": [ + { + "timestamp": "2015-09-15T19:00:00+00:00", + "emails_sent": 198, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-15T20:00:00+00:00", + "emails_sent": 2, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-15T21:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-15T22:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-15T23:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T00:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T01:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T02:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T03:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T04:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T05:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T06:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T07:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T08:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T09:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T10:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T11:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T12:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T13:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T14:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T15:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T16:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T17:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + }, + { + "timestamp": "2015-09-16T18:00:00+00:00", + "emails_sent": 0, + "unique_opens": 0, + "recipients_clicks": 0 + } + ], + "share_report": { + "share_url": "http://usX.vip-reports.net/reports/summary?u=xxxx&id=xxxx", + "share_password": "freddielikesjokes" + }, + "delivery_status": { + "enabled": false + }, + "_links": [ + { + "rel": "parent", + "href": "https://usX.api.mailchimp.com/3.0/reports", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Collection.json", + "schema": "https://api.mailchimp.com/schema/3.0/CollectionLinks/Reports.json" + }, + { + "rel": "self", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Instance.json" + }, + { + "rel": "campaign", + "href": "https://usX.api.mailchimp.com/3.0/campaigns/42694e9e57", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Campaigns/Instance.json" + }, + { + "rel": "sub-reports", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/sub-reports", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Sub/Collection.json" + }, + { + "rel": "abuse-reports", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/abuse-reports", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Abuse/Collection.json" + }, + { + "rel": "advice", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/advice", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Advice/Collection.json" + }, + { + "rel": "click-details", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/click-details", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/ClickDetails/Collection.json" + }, + { + "rel": "domain-performance", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/domain-performance", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/DomainPerformance/Collection.json" + }, + { + "rel": "eepurl", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/eepurl", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Eepurl/Collection.json" + }, + { + "rel": "email-activity", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/email-activity", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/EmailActivity/Collection.json" + }, + { + "rel": "locations", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/locations", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Locations/Collection.json" + }, + { + "rel": "sent-to", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/sent-to", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/SentTo/Collection.json" + }, + { + "rel": "unsubscribed", + "href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/unsubscribed", + "method": "GET", + "targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Unsubs/Collection.json" + } + ] +} +` + +var sampleError = ` +{ + "type": "http://developer.mailchimp.com/documentation/mailchimp/guides/error-glossary/", + "title": "API Key Invalid", + "status": 401, + "detail": "Your API key may be invalid, or you've attempted to access the wrong datacenter.", + "instance": "" +} +`