From 6cabc40e62aa1d8029f153c9609608a0a83da6c8 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Tue, 21 Apr 2020 09:23:00 +0100 Subject: [PATCH] MM-23568: Add rudder to server diagnostics. (#14151) * MM-23568: Add rudder to server diagnostics. * Add unit test. * Go mod tidy. * CSP Header fix. * Fix review comments. * Update web/handlers.go Co-Authored-By: Jesse Hallam * Partially address review comments. * fix tests. * Finish implementing review suggestions and then fixing tests. * Fix CSP Header tests. Co-authored-by: Jesse Hallam --- app/diagnostics.go | 38 +- app/diagnostics_test.go | 209 ++++++++- app/server.go | 40 +- go.mod | 3 +- go.sum | 3 +- .../rudderlabs/analytics-go/.gitignore | 32 ++ .../rudderlabs/analytics-go/.gitmodules | 6 + .../rudderlabs/analytics-go/History.md | 88 ++++ .../rudderlabs/analytics-go/License.md | 21 + .../rudderlabs/analytics-go/Makefile | 31 ++ .../rudderlabs/analytics-go/Readme.md | 55 +++ .../rudderlabs/analytics-go/alias.go | 44 ++ .../rudderlabs/analytics-go/analytics.go | 431 ++++++++++++++++++ .../rudderlabs/analytics-go/config.go | 173 +++++++ .../rudderlabs/analytics-go/context.go | 148 ++++++ .../rudderlabs/analytics-go/error.go | 60 +++ .../rudderlabs/analytics-go/executor.go | 53 +++ .../rudderlabs/analytics-go/group.go | 46 ++ .../rudderlabs/analytics-go/identify.go | 37 ++ .../rudderlabs/analytics-go/integrations.go | 44 ++ .../rudderlabs/analytics-go/json.go | 87 ++++ .../rudderlabs/analytics-go/logger.go | 47 ++ .../rudderlabs/analytics-go/message.go | 131 ++++++ .../rudderlabs/analytics-go/page.go | 38 ++ .../rudderlabs/analytics-go/properties.go | 117 +++++ .../rudderlabs/analytics-go/screen.go | 38 ++ .../rudderlabs/analytics-go/timeout_15.go | 16 + .../rudderlabs/analytics-go/timeout_16.go | 10 + .../rudderlabs/analytics-go/track.go | 46 ++ .../rudderlabs/analytics-go/traits.go | 89 ++++ vendor/modules.txt | 3 + web/handlers.go | 2 +- web/handlers_test.go | 6 +- 33 files changed, 2177 insertions(+), 15 deletions(-) create mode 100644 vendor/github.com/rudderlabs/analytics-go/.gitignore create mode 100644 vendor/github.com/rudderlabs/analytics-go/.gitmodules create mode 100644 vendor/github.com/rudderlabs/analytics-go/History.md create mode 100644 vendor/github.com/rudderlabs/analytics-go/License.md create mode 100644 vendor/github.com/rudderlabs/analytics-go/Makefile create mode 100644 vendor/github.com/rudderlabs/analytics-go/Readme.md create mode 100644 vendor/github.com/rudderlabs/analytics-go/alias.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/analytics.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/config.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/context.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/error.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/executor.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/group.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/identify.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/integrations.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/json.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/logger.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/message.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/page.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/properties.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/screen.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/timeout_15.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/timeout_16.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/track.go create mode 100644 vendor/github.com/rudderlabs/analytics-go/traits.go diff --git a/app/diagnostics.go b/app/diagnostics.go index 7e2d25471e..1290e6af65 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -8,6 +8,7 @@ import ( "runtime" "strings" + rudder "github.com/rudderlabs/analytics-go" "github.com/segmentio/analytics-go" "github.com/mattermost/mattermost-server/v5/mlog" @@ -16,7 +17,9 @@ import ( ) const ( - SEGMENT_KEY = "placeholder_segment_key" + SEGMENT_KEY = "placeholder_segment_key" + RUDDER_KEY = "placeholder_rudder_key" + RUDDER_DATAPLANE_URL = "placeholder_rudder_dataplane_url" TRACK_CONFIG_SERVICE = "config_service" TRACK_CONFIG_TEAM = "config_team" @@ -80,14 +83,37 @@ func (a *App) sendDailyDiagnostics(override bool) { a.trackGroups() a.trackChannelModeration() } + + if *a.Config().LogSettings.EnableDiagnostics && a.IsLeader() && ((!strings.Contains(RUDDER_KEY, "placeholder") && !strings.Contains(RUDDER_DATAPLANE_URL, "placeholder")) || override) { + a.Srv().initRudder(RUDDER_DATAPLANE_URL) + a.trackActivity() + a.trackConfig() + a.trackLicense() + a.trackPlugins() + a.trackServer() + a.trackPermissions() + a.trackElasticsearch() + a.trackGroups() + a.trackChannelModeration() + } } func (a *App) SendDiagnostic(event string, properties map[string]interface{}) { - a.Srv().diagnosticClient.Enqueue(analytics.Track{ - Event: event, - UserId: a.DiagnosticId(), - Properties: properties, - }) + if a.Srv().diagnosticClient != nil { + a.Srv().diagnosticClient.Enqueue(analytics.Track{ + Event: event, + UserId: a.DiagnosticId(), + Properties: properties, + }) + } + + if a.Srv().rudderClient != nil { + a.Srv().rudderClient.Enqueue(rudder.Track{ + Event: event, + UserId: a.DiagnosticId(), + Properties: properties, + }) + } } func isDefault(setting interface{}, defaultValue interface{}) bool { diff --git a/app/diagnostics_test.go b/app/diagnostics_test.go index 5d780e98c8..51396c0e90 100644 --- a/app/diagnostics_test.go +++ b/app/diagnostics_test.go @@ -63,7 +63,7 @@ func TestPluginVersion(t *testing.T) { assert.Empty(t, pluginVersion(plugins, "unknown.plugin")) } -func TestDiagnostics(t *testing.T) { +func TestSegmentDiagnostics(t *testing.T) { if testing.Short() { t.SkipNow() } @@ -275,3 +275,210 @@ func TestDiagnostics(t *testing.T) { } }) } + +func TestRudderDiagnostics(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + + th := SetupWithCustomConfig(t, func(config *model.Config) { + *config.PluginSettings.Enable = false + }) + defer th.TearDown() + + type payload struct { + MessageId string + SentAt time.Time + Batch []struct { + MessageId string + UserId string + Event string + Timestamp time.Time + Properties map[string]interface{} + } + Context struct { + Library struct { + Name string + Version string + } + } + } + + data := make(chan payload, 100) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + + var p payload + err = json.Unmarshal(body, &p) + require.NoError(t, err) + + data <- p + })) + defer server.Close() + + diagnosticID := "test-diagnostic-id-12345" + th.App.SetDiagnosticId(diagnosticID) + th.Server.initRudder(server.URL) + + assertPayload := func(t *testing.T, actual payload, event string, properties map[string]interface{}) { + t.Helper() + assert.NotEmpty(t, actual.MessageId) + assert.False(t, actual.SentAt.IsZero()) + if assert.Len(t, actual.Batch, 1) { + assert.NotEmpty(t, actual.Batch[0].MessageId, "message id should not be empty") + assert.Equal(t, diagnosticID, actual.Batch[0].UserId) + if event != "" { + assert.Equal(t, event, actual.Batch[0].Event) + } + assert.False(t, actual.Batch[0].Timestamp.IsZero(), "batch timestamp should not be the zero value") + if properties != nil { + assert.Equal(t, properties, actual.Batch[0].Properties) + } + } + assert.Equal(t, "analytics-go", actual.Context.Library.Name) + assert.Equal(t, "3.0.0", actual.Context.Library.Version) + } + + collectInfo := func(info *[]string) { + t.Helper() + for { + select { + case result := <-data: + assertPayload(t, result, "", nil) + *info = append(*info, result.Batch[0].Event) + case <-time.After(time.Second * 1): + return + } + } + } + + // Should send a client identify message + select { + case identifyMessage := <-data: + assertPayload(t, identifyMessage, "", nil) + case <-time.After(time.Second * 1): + require.Fail(t, "Did not receive ID message") + } + + t.Run("Send", func(t *testing.T) { + testValue := "test-send-value-6789" + th.App.SendDiagnostic("Testing Diagnostic", map[string]interface{}{ + "hey": testValue, + }) + select { + case result := <-data: + assertPayload(t, result, "Testing Diagnostic", map[string]interface{}{ + "hey": testValue, + }) + case <-time.After(time.Second * 1): + require.Fail(t, "Did not receive diagnostic") + } + }) + + // Plugins remain disabled at this point + t.Run("SendDailyDiagnosticsPluginsDisabled", func(t *testing.T) { + th.App.sendDailyDiagnostics(true) + + var info []string + // Collect the info sent. + collectInfo(&info) + + for _, item := range []string{ + TRACK_CONFIG_SERVICE, + TRACK_CONFIG_TEAM, + TRACK_CONFIG_SQL, + TRACK_CONFIG_LOG, + TRACK_CONFIG_NOTIFICATION_LOG, + TRACK_CONFIG_FILE, + TRACK_CONFIG_RATE, + TRACK_CONFIG_EMAIL, + TRACK_CONFIG_PRIVACY, + TRACK_CONFIG_OAUTH, + TRACK_CONFIG_LDAP, + TRACK_CONFIG_COMPLIANCE, + TRACK_CONFIG_LOCALIZATION, + TRACK_CONFIG_SAML, + TRACK_CONFIG_PASSWORD, + TRACK_CONFIG_CLUSTER, + TRACK_CONFIG_METRICS, + TRACK_CONFIG_SUPPORT, + TRACK_CONFIG_NATIVEAPP, + TRACK_CONFIG_EXPERIMENTAL, + TRACK_CONFIG_ANALYTICS, + TRACK_CONFIG_PLUGIN, + TRACK_ACTIVITY, + TRACK_SERVER, + TRACK_CONFIG_MESSAGE_EXPORT, + // TRACK_PLUGINS, + } { + require.Contains(t, info, item) + } + }) + + // Enable plugins for the remainder of the tests. + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) + + t.Run("SendDailyDiagnostics", func(t *testing.T) { + th.App.sendDailyDiagnostics(true) + + var info []string + // Collect the info sent. + collectInfo(&info) + + for _, item := range []string{ + TRACK_CONFIG_SERVICE, + TRACK_CONFIG_TEAM, + TRACK_CONFIG_SQL, + TRACK_CONFIG_LOG, + TRACK_CONFIG_NOTIFICATION_LOG, + TRACK_CONFIG_FILE, + TRACK_CONFIG_RATE, + TRACK_CONFIG_EMAIL, + TRACK_CONFIG_PRIVACY, + TRACK_CONFIG_OAUTH, + TRACK_CONFIG_LDAP, + TRACK_CONFIG_COMPLIANCE, + TRACK_CONFIG_LOCALIZATION, + TRACK_CONFIG_SAML, + TRACK_CONFIG_PASSWORD, + TRACK_CONFIG_CLUSTER, + TRACK_CONFIG_METRICS, + TRACK_CONFIG_SUPPORT, + TRACK_CONFIG_NATIVEAPP, + TRACK_CONFIG_EXPERIMENTAL, + TRACK_CONFIG_ANALYTICS, + TRACK_CONFIG_PLUGIN, + TRACK_ACTIVITY, + TRACK_SERVER, + TRACK_CONFIG_MESSAGE_EXPORT, + TRACK_PLUGINS, + } { + require.Contains(t, info, item) + } + }) + + t.Run("SendDailyDiagnosticsNoRudderKey", func(t *testing.T) { + th.App.SendDailyDiagnostics() + + select { + case <-data: + require.Fail(t, "Should not send diagnostics when the rudder key is not set") + case <-time.After(time.Second * 1): + // Did not receive diagnostics + } + }) + + t.Run("SendDailyDiagnosticsDisabled", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.LogSettings.EnableDiagnostics = false }) + + th.App.sendDailyDiagnostics(true) + + select { + case <-data: + require.Fail(t, "Should not send diagnostics when they are disabled") + case <-time.After(time.Second * 1): + // Did not receive diagnostics + } + }) +} diff --git a/app/server.go b/app/server.go index 8ffd881293..0e2cd9c44a 100644 --- a/app/server.go +++ b/app/server.go @@ -20,6 +20,7 @@ import ( "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/rs/cors" + rudder "github.com/rudderlabs/analytics-go" analytics "github.com/segmentio/analytics-go" "github.com/throttled/throttled" "golang.org/x/crypto/acme/autocert" @@ -115,6 +116,7 @@ type Server struct { diagnosticId string diagnosticClient analytics.Client + rudderClient rudder.Client phase2PermissionsMigrationComplete bool @@ -874,13 +876,47 @@ func (s *Server) initDiagnostics(endpoint string) { } } +func (s *Server) initRudder(endpoint string) { + if s.rudderClient == nil { + config := rudder.Config{} + config.Logger = rudder.StdLogger(s.Log.StdLog(mlog.String("source", "rudder"))) + config.Endpoint = endpoint + // For testing + if endpoint != RUDDER_DATAPLANE_URL { + config.Verbose = true + config.BatchSize = 1 + } + client, err := rudder.NewWithConfig(RUDDER_KEY, config) + if err != nil { + mlog.Error("Failed to create Rudder instance", mlog.Err(err)) + return + } + client.Enqueue(rudder.Identify{ + UserId: s.diagnosticId, + }) + + s.rudderClient = client + } +} + // shutdownDiagnostics closes the diagnostic client. func (s *Server) shutdownDiagnostics() error { + var segmentErr, rudderErr error if s.diagnosticClient != nil { - return s.diagnosticClient.Close() + segmentErr = s.diagnosticClient.Close() } - return nil + if s.rudderClient != nil { + rudderErr = s.rudderClient.Close() + } + + if segmentErr != nil && rudderErr != nil { + return errors.New(fmt.Sprintf("%s, %s", segmentErr.Error(), rudderErr.Error())) + } else if segmentErr != nil { + return segmentErr + } + + return rudderErr } // GetHubs returns the list of hubs. This method is safe diff --git a/go.mod b/go.mod index fb6b8ba48c..e53ebb3476 100644 --- a/go.mod +++ b/go.mod @@ -72,7 +72,8 @@ require ( github.com/prometheus/client_golang v1.4.0 github.com/prometheus/client_model v0.2.0 // indirect github.com/rs/cors v1.7.0 - github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 // indirect + github.com/rudderlabs/analytics-go v3.1.0+incompatible + github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/segmentio/analytics-go v3.1.0+incompatible github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c // indirect diff --git a/go.sum b/go.sum index c682d5ec91..970aaafc04 100644 --- a/go.sum +++ b/go.sum @@ -282,7 +282,6 @@ github.com/mattermost/gosaml2 v0.3.2 h1:kq2dY5qUe6fPPHra171GVlgo+ycBsEog0gZMetxL github.com/mattermost/gosaml2 v0.3.2/go.mod h1:Z429EIOiEi9kbq6yHoApfzlcXpa6dzRDc6pO+Vy2Ksk= github.com/mattermost/ldap v0.0.0-20191128190019-9f62ba4b8d4d h1:2DV7VIlEv6J5R5o6tUcb3ZMKJYeeZuWZL7Rv1m23TgQ= github.com/mattermost/ldap v0.0.0-20191128190019-9f62ba4b8d4d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ= -github.com/mattermost/mattermost-server v5.11.1+incompatible h1:LPzKY0+2Tic/ik67qIg6VrydRCgxNXZQXOeaiJ2rMBY= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= github.com/mattermost/viper v1.0.4 h1:cMYOz4PhguscGSPxrSokUtib5HrG4gCpiUh27wyA3d0= @@ -412,6 +411,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rudderlabs/analytics-go v3.1.0+incompatible h1:raJgBJMxxnCe9CWHL1zJq4rccap4yG7lvk4Fs6B7v+o= +github.com/rudderlabs/analytics-go v3.1.0+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 h1:J4AOUcOh/t1XbQcJfkEqhzgvMJ2tDxdCVvmHxW5QXao= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= diff --git a/vendor/github.com/rudderlabs/analytics-go/.gitignore b/vendor/github.com/rudderlabs/analytics-go/.gitignore new file mode 100644 index 0000000000..942678bd91 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/.gitignore @@ -0,0 +1,32 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# Emacs +*~ +\#* +.\#* + +# Artifacts +tmp/* diff --git a/vendor/github.com/rudderlabs/analytics-go/.gitmodules b/vendor/github.com/rudderlabs/analytics-go/.gitmodules new file mode 100644 index 0000000000..b2150b3427 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/.gitmodules @@ -0,0 +1,6 @@ +[submodule "vendor/github.com/segmentio/backo-go"] + path = vendor/github.com/segmentio/backo-go + url = https://github.com/segmentio/backo-go +[submodule "vendor/github.com/xtgo/uuid"] + path = vendor/github.com/xtgo/uuid + url = https://github.com/xtgo/uuid diff --git a/vendor/github.com/rudderlabs/analytics-go/History.md b/vendor/github.com/rudderlabs/analytics-go/History.md new file mode 100644 index 0000000000..d01214e65c --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/History.md @@ -0,0 +1,88 @@ + +v3.1.0 / 2019-09-20 +=================== + + * add consistent panic error message + * Expose the Message interface Validate method + * return error if a custom type is enqueued + * Handle pointer types in Enqueue() + * message: update maxMessageBytes to 32KB + +v3.0.1 / 2018-10-02 +=================== + +* Migrate from Circle V1 format to Circle V2 +* Adds CLI for sending segment events +* Vendor packages back-go and uuid instead of using gitsubmodules + + +v3.0.0 / 2016-06-02 +=================== + + * 3.0 is a significant rewrite with multiple breaking changes. + * [Quickstart](https://segment.com/docs/sources/server/go/quickstart/). + * [Documentation](https://segment.com/docs/sources/server/go/). + * [GoDocs](https://godoc.org/gopkg.in/segmentio/analytics-go.v3). + * [What's New in v3](https://segment.com/docs/sources/server/go/#what-s-new-in-v3). + + +v2.1.0 / 2015-12-28 +=================== + + * Add ability to set custom timestamps for messages. + * Add ability to set a custom `net/http` client. + * Add ability to set a custom logger. + * Fix edge case when client would try to upload no messages. + * Properly upload in-flight messages when client is asked to shutdown. + * Add ability to set `.integrations` field on messages. + * Fix resource leak with interval ticker after shutdown. + * Add retries and back-off when uploading messages. + * Add ability to set custom flush interval. + +v2.0.0 / 2015-02-03 +=================== + + * rewrite with breaking API changes + +v1.2.0 / 2014-09-03 +================== + + * add public .Flush() method + * rename .Stop() to .Close() + +v1.1.0 / 2014-09-02 +================== + + * add client.Stop() to flash/wait. Closes #7 + +v1.0.0 / 2014-08-26 +================== + + * fix response close + * change comments to be more go-like + * change uuid libraries + +0.1.2 / 2014-06-11 +================== + + * add runnable example + * fix: close body + +0.1.1 / 2014-05-31 +================== + + * refactor locking + +0.1.0 / 2014-05-22 +================== + + * replace Debug option with debug package + +0.0.2 / 2014-05-20 +================== + + * add .Start() + * add mutexes + * rename BufferSize to FlushAt and FlushInterval to FlushAfter + * lower FlushInterval to 5 seconds + * lower BufferSize to 20 to match other clients diff --git a/vendor/github.com/rudderlabs/analytics-go/License.md b/vendor/github.com/rudderlabs/analytics-go/License.md new file mode 100644 index 0000000000..f452c5d0fd --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/License.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Segment, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/rudderlabs/analytics-go/Makefile b/vendor/github.com/rudderlabs/analytics-go/Makefile new file mode 100644 index 0000000000..9de0d1ba74 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/Makefile @@ -0,0 +1,31 @@ +ifndef CIRCLE_ARTIFACTS +CIRCLE_ARTIFACTS=tmp +endif + +bootstrap: + .buildscript/bootstrap.sh + +dependencies: + @go get -v -t ./... + +vet: + @go vet ./... + +test: vet + @mkdir -p ${CIRCLE_ARTIFACTS} + @go test -race -coverprofile=${CIRCLE_ARTIFACTS}/cover.out . + @go tool cover -func ${CIRCLE_ARTIFACTS}/cover.out -o ${CIRCLE_ARTIFACTS}/cover.txt + @go tool cover -html ${CIRCLE_ARTIFACTS}/cover.out -o ${CIRCLE_ARTIFACTS}/cover.html + +build: test + @go build ./... + +e2e: + @if [ "$(RUN_E2E_TESTS)" != "true" ]; then \ + echo "Skipping end to end tests."; else \ + go get github.com/segmentio/library-e2e-tester/cmd/tester; \ + tester -segment-write-key=$(SEGMENT_WRITE_KEY) -webhook-auth-username=$(WEBHOOK_AUTH_USERNAME) -webhook-bucket=$(WEBHOOK_BUCKET) -path='cli' -concurrency=2 -skip='advance|alias'; fi + +ci: dependencies test e2e + +.PHONY: bootstrap dependencies vet test e2e ci diff --git a/vendor/github.com/rudderlabs/analytics-go/Readme.md b/vendor/github.com/rudderlabs/analytics-go/Readme.md new file mode 100644 index 0000000000..7d369c16e8 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/Readme.md @@ -0,0 +1,55 @@ +# analytics-go [![Circle CI](https://circleci.com/gh/segmentio/analytics-go/tree/master.svg?style=shield)](https://circleci.com/gh/segmentio/analytics-go/tree/master) [![go-doc](https://godoc.org/github.com/segmentio/analytics-go?status.svg)](https://godoc.org/github.com/segmentio/analytics-go) + +Segment analytics client for Go. + +## Installation + +The package can be simply installed via go get, we recommend that you use a +package version management system like the Go vendor directory or a tool like +Godep to avoid issues related to API breaking changes introduced between major +versions of the library. + +To install it in the GOPATH: +``` +go get https://github.com/segmentio/analytics-go +``` + +## Documentation + +The links bellow should provide all the documentation needed to make the best +use of the library and the Segment API: + +- [Documentation](https://segment.com/docs/libraries/go/) +- [godoc](https://godoc.org/gopkg.in/segmentio/analytics-go.v3) +- [API](https://segment.com/docs/libraries/http/) +- [Specs](https://segment.com/docs/spec/) + +## Usage + +```go +package main + +import ( + "os" + + "github.com/segmentio/analytics-go" +) + +func main() { + // Instantiates a client to use send messages to the segment API. + client := analytics.New(os.Getenv("SEGMENT_WRITE_KEY")) + + // Enqueues a track event that will be sent asynchronously. + client.Enqueue(analytics.Track{ + UserId: "test-user", + Event: "test-snippet", + }) + + // Flushes any queued messages and closes the client. + client.Close() +} +``` + +## License + +The library is released under the [MIT license](License.md). diff --git a/vendor/github.com/rudderlabs/analytics-go/alias.go b/vendor/github.com/rudderlabs/analytics-go/alias.go new file mode 100644 index 0000000000..e9dbe85aa0 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/alias.go @@ -0,0 +1,44 @@ +package analytics + +import "time" + +var _ Message = (*Alias)(nil) + +// This type represents object sent in a alias call as described in +// https://segment.com/docs/libraries/http/#alias +type Alias struct { + // This field is exported for serialization purposes and shouldn't be set by + // the application, its value is always overwritten by the library. + Type string `json:"type,omitempty"` + + MessageId string `json:"messageId,omitempty"` + PreviousId string `json:"previousId"` + UserId string `json:"userId"` + Timestamp time.Time `json:"timestamp,omitempty"` + Context *Context `json:"context,omitempty"` + Integrations Integrations `json:"integrations,omitempty"` +} + +func (msg Alias) internal() { + panic(unimplementedError) +} + +func (msg Alias) Validate() error { + if len(msg.UserId) == 0 { + return FieldError{ + Type: "analytics.Alias", + Name: "UserId", + Value: msg.UserId, + } + } + + if len(msg.PreviousId) == 0 { + return FieldError{ + Type: "analytics.Alias", + Name: "PreviousId", + Value: msg.PreviousId, + } + } + + return nil +} diff --git a/vendor/github.com/rudderlabs/analytics-go/analytics.go b/vendor/github.com/rudderlabs/analytics-go/analytics.go new file mode 100644 index 0000000000..d0f089fb6c --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/analytics.go @@ -0,0 +1,431 @@ +package analytics + +import ( + "fmt" + "io" + "io/ioutil" + "sync" + + "bytes" + "encoding/json" + "net/http" + "time" +) + +// Version of the client. +const Version = "3.0.0" +const unimplementedError = "not implemented" + +// This interface is the main API exposed by the analytics package. +// Values that satsify this interface are returned by the client constructors +// provided by the package and provide a way to send messages via the HTTP API. +type Client interface { + io.Closer + + // Queues a message to be sent by the client when the conditions for a batch + // upload are met. + // This is the main method you'll be using, a typical flow would look like + // this: + // + // client := analytics.New(writeKey) + // ... + // client.Enqueue(analytics.Track{ ... }) + // ... + // client.Close() + // + // The method returns an error if the message queue not be queued, which + // happens if the client was already closed at the time the method was + // called or if the message was malformed. + Enqueue(Message) error +} + +type client struct { + Config + key string + + // This channel is where the `Enqueue` method writes messages so they can be + // picked up and pushed by the backend goroutine taking care of applying the + // batching rules. + msgs chan Message + + // These two channels are used to synchronize the client shutting down when + // `Close` is called. + // The first channel is closed to signal the backend goroutine that it has + // to stop, then the second one is closed by the backend goroutine to signal + // that it has finished flushing all queued messages. + quit chan struct{} + shutdown chan struct{} + + // This HTTP client is used to send requests to the backend, it uses the + // HTTP transport provided in the configuration. + http http.Client +} + +// Instantiate a new client that uses the write key passed as first argument to +// send messages to the backend. +// The client is created with the default configuration. +func New(writeKey string) Client { + // Here we can ignore the error because the default config is always valid. + c, _ := NewWithConfig(writeKey, Config{}) + return c +} + +// Instantiate a new client that uses the write key and configuration passed as +// arguments to send messages to the backend. +// The function will return an error if the configuration contained impossible +// values (like a negative flush interval for example). +// When the function returns an error the returned client will always be nil. +func NewWithConfig(writeKey string, config Config) (cli Client, err error) { + if err = config.validate(); err != nil { + return + } + + c := &client{ + Config: makeConfig(config), + key: writeKey, + msgs: make(chan Message, 100), + quit: make(chan struct{}), + shutdown: make(chan struct{}), + http: makeHttpClient(config.Transport), + } + + go c.loop() + + cli = c + return +} + +func makeHttpClient(transport http.RoundTripper) http.Client { + httpClient := http.Client{ + Transport: transport, + } + if supportsTimeout(transport) { + httpClient.Timeout = 10 * time.Second + } + return httpClient +} + +func dereferenceMessage(msg Message) Message { + switch m := msg.(type) { + case *Alias: + if m == nil { + return nil + } + return *m + case *Group: + if m == nil { + return nil + } + return *m + case *Identify: + if m == nil { + return nil + } + return *m + case *Page: + if m == nil { + return nil + } + return *m + case *Screen: + if m == nil { + return nil + } + return *m + case *Track: + if m == nil { + return nil + } + return *m + } + + return msg +} + +func (c *client) Enqueue(msg Message) (err error) { + msg = dereferenceMessage(msg) + if err = msg.Validate(); err != nil { + return + } + + var id = c.uid() + var ts = c.now() + + switch m := msg.(type) { + case Alias: + m.Type = "alias" + m.MessageId = makeMessageId(m.MessageId, id) + m.Timestamp = makeTimestamp(m.Timestamp, ts) + msg = m + + case Group: + m.Type = "group" + m.MessageId = makeMessageId(m.MessageId, id) + m.Timestamp = makeTimestamp(m.Timestamp, ts) + msg = m + + case Identify: + m.Type = "identify" + m.MessageId = makeMessageId(m.MessageId, id) + m.Timestamp = makeTimestamp(m.Timestamp, ts) + msg = m + + case Page: + m.Type = "page" + m.MessageId = makeMessageId(m.MessageId, id) + m.Timestamp = makeTimestamp(m.Timestamp, ts) + msg = m + + case Screen: + m.Type = "screen" + m.MessageId = makeMessageId(m.MessageId, id) + m.Timestamp = makeTimestamp(m.Timestamp, ts) + msg = m + + case Track: + m.Type = "track" + m.MessageId = makeMessageId(m.MessageId, id) + m.Timestamp = makeTimestamp(m.Timestamp, ts) + msg = m + + default: + err = fmt.Errorf("messages with custom types cannot be enqueued: %T", msg) + return + } + + defer func() { + // When the `msgs` channel is closed writing to it will trigger a panic. + // To avoid letting the panic propagate to the caller we recover from it + // and instead report that the client has been closed and shouldn't be + // used anymore. + if recover() != nil { + err = ErrClosed + } + }() + + c.msgs <- msg + return +} + +// Close and flush metrics. +func (c *client) Close() (err error) { + defer func() { + // Always recover, a panic could be raised if `c`.quit was closed which + // means the method was called more than once. + if recover() != nil { + err = ErrClosed + } + }() + close(c.quit) + <-c.shutdown + return +} + +// Asychronously send a batched requests. +func (c *client) sendAsync(msgs []message, wg *sync.WaitGroup, ex *executor) { + wg.Add(1) + + if !ex.do(func() { + defer wg.Done() + defer func() { + // In case a bug is introduced in the send function that triggers + // a panic, we don't want this to ever crash the application so we + // catch it here and log it instead. + if err := recover(); err != nil { + c.errorf("panic - %s", err) + } + }() + c.send(msgs) + }) { + wg.Done() + c.errorf("sending messages failed - %s", ErrTooManyRequests) + c.notifyFailure(msgs, ErrTooManyRequests) + } +} + +// Send batch request. +func (c *client) send(msgs []message) { + const attempts = 10 + + b, err := json.Marshal(batch{ + MessageId: c.uid(), + SentAt: c.now(), + Messages: msgs, + Context: c.DefaultContext, + }) + + if err != nil { + c.errorf("marshalling messages - %s", err) + c.notifyFailure(msgs, err) + return + } + + for i := 0; i != attempts; i++ { + if err = c.upload(b); err == nil { + c.notifySuccess(msgs) + return + } + + // Wait for either a retry timeout or the client to be closed. + select { + case <-time.After(c.RetryAfter(i)): + case <-c.quit: + c.errorf("%d messages dropped because they failed to be sent and the client was closed", len(msgs)) + c.notifyFailure(msgs, err) + return + } + } + + c.errorf("%d messages dropped because they failed to be sent after %d attempts", len(msgs), attempts) + c.notifyFailure(msgs, err) +} + +// Upload serialized batch message. +func (c *client) upload(b []byte) error { + url := c.Endpoint + "/v1/batch" + req, err := http.NewRequest("POST", url, bytes.NewReader(b)) + if err != nil { + c.errorf("creating request - %s", err) + return err + } + + req.Header.Add("User-Agent", "analytics-go (version: "+Version+")") + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Content-Length", string(len(b))) + req.SetBasicAuth(c.key, "") + + res, err := c.http.Do(req) + + if err != nil { + c.errorf("sending request - %s", err) + return err + } + + defer res.Body.Close() + return c.report(res) +} + +// Report on response body. +func (c *client) report(res *http.Response) (err error) { + var body []byte + + if res.StatusCode < 300 { + c.debugf("response %s", res.Status) + return + } + + if body, err = ioutil.ReadAll(res.Body); err != nil { + c.errorf("response %d %s - %s", res.StatusCode, res.Status, err) + return + } + + c.logf("response %d %s – %s", res.StatusCode, res.Status, string(body)) + return fmt.Errorf("%d %s", res.StatusCode, res.Status) +} + +// Batch loop. +func (c *client) loop() { + defer close(c.shutdown) + + wg := &sync.WaitGroup{} + defer wg.Wait() + + tick := time.NewTicker(c.Interval) + defer tick.Stop() + + ex := newExecutor(c.maxConcurrentRequests) + defer ex.close() + + mq := messageQueue{ + maxBatchSize: c.BatchSize, + maxBatchBytes: c.maxBatchBytes(), + } + + for { + select { + case msg := <-c.msgs: + c.push(&mq, msg, wg, ex) + + case <-tick.C: + c.flush(&mq, wg, ex) + + case <-c.quit: + c.debugf("exit requested – draining messages") + + // Drain the msg channel, we have to close it first so no more + // messages can be pushed and otherwise the loop would never end. + close(c.msgs) + for msg := range c.msgs { + c.push(&mq, msg, wg, ex) + } + + c.flush(&mq, wg, ex) + c.debugf("exit") + return + } + } +} + +func (c *client) push(q *messageQueue, m Message, wg *sync.WaitGroup, ex *executor) { + var msg message + var err error + + if msg, err = makeMessage(m, maxMessageBytes); err != nil { + c.errorf("%s - %v", err, m) + c.notifyFailure([]message{{m, nil}}, err) + return + } + + c.debugf("buffer (%d/%d) %v", len(q.pending), c.BatchSize, m) + + if msgs := q.push(msg); msgs != nil { + c.debugf("exceeded messages batch limit with batch of %d messages – flushing", len(msgs)) + c.sendAsync(msgs, wg, ex) + } +} + +func (c *client) flush(q *messageQueue, wg *sync.WaitGroup, ex *executor) { + if msgs := q.flush(); msgs != nil { + c.debugf("flushing %d messages", len(msgs)) + c.sendAsync(msgs, wg, ex) + } +} + +func (c *client) debugf(format string, args ...interface{}) { + if c.Verbose { + c.logf(format, args...) + } +} + +func (c *client) logf(format string, args ...interface{}) { + c.Logger.Logf(format, args...) +} + +func (c *client) errorf(format string, args ...interface{}) { + c.Logger.Errorf(format, args...) +} + +func (c *client) maxBatchBytes() int { + b, _ := json.Marshal(batch{ + MessageId: c.uid(), + SentAt: c.now(), + Context: c.DefaultContext, + }) + return maxBatchBytes - len(b) +} + +func (c *client) notifySuccess(msgs []message) { + if c.Callback != nil { + for _, m := range msgs { + c.Callback.Success(m.msg) + } + } +} + +func (c *client) notifyFailure(msgs []message, err error) { + if c.Callback != nil { + for _, m := range msgs { + c.Callback.Failure(m.msg, err) + } + } +} diff --git a/vendor/github.com/rudderlabs/analytics-go/config.go b/vendor/github.com/rudderlabs/analytics-go/config.go new file mode 100644 index 0000000000..c167757304 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/config.go @@ -0,0 +1,173 @@ +package analytics + +import ( + "net/http" + "time" + + "github.com/segmentio/backo-go" + "github.com/xtgo/uuid" +) + +// Instances of this type carry the different configuration options that may +// be set when instantiating a client. +// +// Each field's zero-value is either meaningful or interpreted as using the +// default value defined by the library. +type Config struct { + + // The endpoint to which the client connect and send their messages, set to + // `DefaultEndpoint` by default. + Endpoint string + + // The flushing interval of the client. Messages will be sent when they've + // been queued up to the maximum batch size or when the flushing interval + // timer triggers. + Interval time.Duration + + // The HTTP transport used by the client, this allows an application to + // redefine how requests are being sent at the HTTP level (for example, + // to change the connection pooling policy). + // If none is specified the client uses `http.DefaultTransport`. + Transport http.RoundTripper + + // The logger used by the client to output info or error messages when that + // are generated by background operations. + // If none is specified the client uses a standard logger that outputs to + // `os.Stderr`. + Logger Logger + + // The callback object that will be used by the client to notify the + // application when messages sends to the backend API succeeded or failed. + Callback Callback + + // The maximum number of messages that will be sent in one API call. + // Messages will be sent when they've been queued up to the maximum batch + // size or when the flushing interval timer triggers. + // Note that the API will still enforce a 500KB limit on each HTTP request + // which is independent from the number of embedded messages. + BatchSize int + + // When set to true the client will send more frequent and detailed messages + // to its logger. + Verbose bool + + // The default context set on each message sent by the client. + DefaultContext *Context + + // The retry policy used by the client to resend requests that have failed. + // The function is called with how many times the operation has been retried + // and is expected to return how long the client should wait before trying + // again. + // If not set the client will fallback to use a default retry policy. + RetryAfter func(int) time.Duration + + // A function called by the client to generate unique message identifiers. + // The client uses a UUID generator if none is provided. + // This field is not exported and only exposed internally to let unit tests + // mock the id generation. + uid func() string + + // A function called by the client to get the current time, `time.Now` is + // used by default. + // This field is not exported and only exposed internally to let unit tests + // mock the current time. + now func() time.Time + + // The maximum number of goroutines that will be spawned by a client to send + // requests to the backend API. + // This field is not exported and only exposed internally to let unit tests + // mock the current time. + maxConcurrentRequests int +} + +// This constant sets the default endpoint to which client instances send +// messages if none was explictly set. +const DefaultEndpoint = "https://api.segment.io" + +// This constant sets the default flush interval used by client instances if +// none was explicitly set. +const DefaultInterval = 5 * time.Second + +// This constant sets the default batch size used by client instances if none +// was explicitly set. +const DefaultBatchSize = 250 + +// Verifies that fields that don't have zero-values are set to valid values, +// returns an error describing the problem if a field was invalid. +func (c *Config) validate() error { + if c.Interval < 0 { + return ConfigError{ + Reason: "negative time intervals are not supported", + Field: "Interval", + Value: c.Interval, + } + } + + if c.BatchSize < 0 { + return ConfigError{ + Reason: "negative batch sizes are not supported", + Field: "BatchSize", + Value: c.BatchSize, + } + } + + return nil +} + +// Given a config object as argument the function will set all zero-values to +// their defaults and return the modified object. +func makeConfig(c Config) Config { + if len(c.Endpoint) == 0 { + c.Endpoint = DefaultEndpoint + } + + if c.Interval == 0 { + c.Interval = DefaultInterval + } + + if c.Transport == nil { + c.Transport = http.DefaultTransport + } + + if c.Logger == nil { + c.Logger = newDefaultLogger() + } + + if c.BatchSize == 0 { + c.BatchSize = DefaultBatchSize + } + + if c.DefaultContext == nil { + c.DefaultContext = &Context{} + } + + if c.RetryAfter == nil { + c.RetryAfter = backo.DefaultBacko().Duration + } + + if c.uid == nil { + c.uid = uid + } + + if c.now == nil { + c.now = time.Now + } + + if c.maxConcurrentRequests == 0 { + c.maxConcurrentRequests = 1000 + } + + // We always overwrite the 'library' field of the default context set on the + // client because we want this information to be accurate. + c.DefaultContext.Library = LibraryInfo{ + Name: "analytics-go", + Version: Version, + } + return c +} + +// This function returns a string representation of a UUID, it's the default +// function used for generating unique IDs. +func uid() string { + return uuid.NewRandom().String() +} diff --git a/vendor/github.com/rudderlabs/analytics-go/context.go b/vendor/github.com/rudderlabs/analytics-go/context.go new file mode 100644 index 0000000000..7a30c1b98d --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/context.go @@ -0,0 +1,148 @@ +package analytics + +import ( + "encoding/json" + "net" + "reflect" +) + +// This type provides the representation of the `context` object as defined in +// https://segment.com/docs/spec/common/#context +type Context struct { + App AppInfo `json:"app,omitempty"` + Campaign CampaignInfo `json:"campaign,omitempty"` + Device DeviceInfo `json:"device,omitempty"` + Library LibraryInfo `json:"library,omitempty"` + Location LocationInfo `json:"location,omitempty"` + Network NetworkInfo `json:"network,omitempty"` + OS OSInfo `json:"os,omitempty"` + Page PageInfo `json:"page,omitempty"` + Referrer ReferrerInfo `json:"referrer,omitempty"` + Screen ScreenInfo `json:"screen,omitempty"` + IP net.IP `json:"ip,omitempty"` + Locale string `json:"locale,omitempty"` + Timezone string `json:"timezone,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + Traits Traits `json:"traits,omitempty"` + + // This map is used to allow extensions to the context specifications that + // may not be documented or could be introduced in the future. + // The fields of this map are inlined in the serialized context object, + // there is no actual "extra" field in the JSON representation. + Extra map[string]interface{} `json:"-"` +} + +// This type provides the representation of the `context.app` object as defined +// in https://segment.com/docs/spec/common/#context +type AppInfo struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + Build string `json:"build,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + +// This type provides the representation of the `context.campaign` object as +// defined in https://segment.com/docs/spec/common/#context +type CampaignInfo struct { + Name string `json:"name,omitempty"` + Source string `json:"source,omitempty"` + Medium string `json:"medium,omitempty"` + Term string `json:"term,omitempty"` + Content string `json:"content,omitempty"` +} + +// This type provides the representation of the `context.device` object as +// defined in https://segment.com/docs/spec/common/#context +type DeviceInfo struct { + Id string `json:"id,omitempty"` + Manufacturer string `json:"manufacturer,omitempty"` + Model string `json:"model,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Version string `json:"version,omitempty"` + AdvertisingID string `json:"advertisingId,omitempty"` +} + +// This type provides the representation of the `context.library` object as +// defined in https://segment.com/docs/spec/common/#context +type LibraryInfo struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` +} + +// This type provides the representation of the `context.location` object as +// defined in https://segment.com/docs/spec/common/#context +type LocationInfo struct { + City string `json:"city,omitempty"` + Country string `json:"country,omitempty"` + Region string `json:"region,omitempty"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` + Speed float64 `json:"speed,omitempty"` +} + +// This type provides the representation of the `context.network` object as +// defined in https://segment.com/docs/spec/common/#context +type NetworkInfo struct { + Bluetooth bool `json:"bluetooth,omitempty"` + Cellular bool `json:"cellular,omitempty"` + WIFI bool `json:"wifi,omitempty"` + Carrier string `json:"carrier,omitempty"` +} + +// This type provides the representation of the `context.os` object as defined +// in https://segment.com/docs/spec/common/#context +type OSInfo struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` +} + +// This type provides the representation of the `context.page` object as +// defined in https://segment.com/docs/spec/common/#context +type PageInfo struct { + Hash string `json:"hash,omitempty"` + Path string `json:"path,omitempty"` + Referrer string `json:"referrer,omitempty"` + Search string `json:"search,omitempty"` + Title string `json:"title,omitempty"` + URL string `json:"url,omitempty"` +} + +// This type provides the representation of the `context.referrer` object as +// defined in https://segment.com/docs/spec/common/#context +type ReferrerInfo struct { + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + URL string `json:"url,omitempty"` + Link string `json:"link,omitempty"` +} + +// This type provides the representation of the `context.screen` object as +// defined in https://segment.com/docs/spec/common/#context +type ScreenInfo struct { + Density int `json:"density,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// Satisfy the `json.Marshaler` interface. We have to flatten out the `Extra` +// field but the standard json package doesn't support it yet. +// Implementing this interface allows us to override the default marshaling of +// the context object and to the inlining ourselves. +// +// Related discussion: https://github.com/golang/go/issues/6213 +func (ctx Context) MarshalJSON() ([]byte, error) { + v := reflect.ValueOf(ctx) + n := v.NumField() + m := make(map[string]interface{}, n+len(ctx.Extra)) + + // Copy the `Extra` map into the map representation of the context, it is + // important to do this operation before going through the actual struct + // fields so the latter take precendence and override duplicated values + // that would be set in the extensions. + for name, value := range ctx.Extra { + m[name] = value + } + + return json.Marshal(structToMap(v, m)) +} diff --git a/vendor/github.com/rudderlabs/analytics-go/error.go b/vendor/github.com/rudderlabs/analytics-go/error.go new file mode 100644 index 0000000000..d5503864e9 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/error.go @@ -0,0 +1,60 @@ +package analytics + +import ( + "errors" + "fmt" +) + +// Returned by the `NewWithConfig` function when the one of the configuration +// fields was set to an impossible value (like a negative duration). +type ConfigError struct { + + // A human-readable message explaining why the configuration field's value + // is invalid. + Reason string + + // The name of the configuration field that was carrying an invalid value. + Field string + + // The value of the configuration field that caused the error. + Value interface{} +} + +func (e ConfigError) Error() string { + return fmt.Sprintf("analytics.NewWithConfig: %s (analytics.Config.%s: %#v)", e.Reason, e.Field, e.Value) +} + +// Instances of this type are used to represent errors returned when a field was +// no initialize properly in a structure passed as argument to one of the +// functions of this package. +type FieldError struct { + + // The human-readable representation of the type of structure that wasn't + // initialized properly. + Type string + + // The name of the field that wasn't properly initialized. + Name string + + // The value of the field that wasn't properly initialized. + Value interface{} +} + +func (e FieldError) Error() string { + return fmt.Sprintf("%s.%s: invalid field value: %#v", e.Type, e.Name, e.Value) +} + +var ( + // This error is returned by methods of the `Client` interface when they are + // called after the client was already closed. + ErrClosed = errors.New("the client was already closed") + + // This error is used to notify the application that too many requests are + // already being sent and no more messages can be accepted. + ErrTooManyRequests = errors.New("too many requests are already in-flight") + + // This error is used to notify the client callbacks that a message send + // failed because the JSON representation of a message exceeded the upper + // limit. + ErrMessageTooBig = errors.New("the message exceeds the maximum allowed size") +) diff --git a/vendor/github.com/rudderlabs/analytics-go/executor.go b/vendor/github.com/rudderlabs/analytics-go/executor.go new file mode 100644 index 0000000000..405ee98eef --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/executor.go @@ -0,0 +1,53 @@ +package analytics + +import "sync" + +type executor struct { + queue chan func() + mutex sync.Mutex + size int + cap int +} + +func newExecutor(cap int) *executor { + e := &executor{ + queue: make(chan func(), 1), + cap: cap, + } + go e.loop() + return e +} + +func (e *executor) do(task func()) (ok bool) { + e.mutex.Lock() + + if e.size != e.cap { + e.queue <- task + e.size++ + ok = true + } + + e.mutex.Unlock() + return +} + +func (e *executor) close() { + close(e.queue) +} + +func (e *executor) loop() { + for task := range e.queue { + go e.run(task) + } +} + +func (e *executor) run(task func()) { + defer e.done() + task() +} + +func (e *executor) done() { + e.mutex.Lock() + e.size-- + e.mutex.Unlock() +} diff --git a/vendor/github.com/rudderlabs/analytics-go/group.go b/vendor/github.com/rudderlabs/analytics-go/group.go new file mode 100644 index 0000000000..1da00faaa9 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/group.go @@ -0,0 +1,46 @@ +package analytics + +import "time" + +var _ Message = (*Group)(nil) + +// This type represents object sent in a group call as described in +// https://segment.com/docs/libraries/http/#group +type Group struct { + // This field is exported for serialization purposes and shouldn't be set by + // the application, its value is always overwritten by the library. + Type string `json:"type,omitempty"` + + MessageId string `json:"messageId,omitempty"` + AnonymousId string `json:"anonymousId,omitempty"` + UserId string `json:"userId,omitempty"` + GroupId string `json:"groupId"` + Timestamp time.Time `json:"timestamp,omitempty"` + Context *Context `json:"context,omitempty"` + Traits Traits `json:"traits,omitempty"` + Integrations Integrations `json:"integrations,omitempty"` +} + +func (msg Group) internal() { + panic(unimplementedError) +} + +func (msg Group) Validate() error { + if len(msg.GroupId) == 0 { + return FieldError{ + Type: "analytics.Group", + Name: "GroupId", + Value: msg.GroupId, + } + } + + if len(msg.UserId) == 0 && len(msg.AnonymousId) == 0 { + return FieldError{ + Type: "analytics.Group", + Name: "UserId", + Value: msg.UserId, + } + } + + return nil +} diff --git a/vendor/github.com/rudderlabs/analytics-go/identify.go b/vendor/github.com/rudderlabs/analytics-go/identify.go new file mode 100644 index 0000000000..ab078aa088 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/identify.go @@ -0,0 +1,37 @@ +package analytics + +import "time" + +var _ Message = (*Identify)(nil) + +// This type represents object sent in an identify call as described in +// https://segment.com/docs/libraries/http/#identify +type Identify struct { + // This field is exported for serialization purposes and shouldn't be set by + // the application, its value is always overwritten by the library. + Type string `json:"type,omitempty"` + + MessageId string `json:"messageId,omitempty"` + AnonymousId string `json:"anonymousId,omitempty"` + UserId string `json:"userId,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` + Context *Context `json:"context,omitempty"` + Traits Traits `json:"traits,omitempty"` + Integrations Integrations `json:"integrations,omitempty"` +} + +func (msg Identify) internal() { + panic(unimplementedError) +} + +func (msg Identify) Validate() error { + if len(msg.UserId) == 0 && len(msg.AnonymousId) == 0 { + return FieldError{ + Type: "analytics.Identify", + Name: "UserId", + Value: msg.UserId, + } + } + + return nil +} diff --git a/vendor/github.com/rudderlabs/analytics-go/integrations.go b/vendor/github.com/rudderlabs/analytics-go/integrations.go new file mode 100644 index 0000000000..407b4912bc --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/integrations.go @@ -0,0 +1,44 @@ +package analytics + +// This type is used to represent integrations in messages that support it. +// It is a free-form where values are most often booleans that enable or +// disable integrations. +// Here's a quick example of how this type is meant to be used: +// +// analytics.Track{ +// UserId: "0123456789", +// Integrations: analytics.NewIntegrations() +// .EnableAll() +// .Disable("Salesforce") +// .Disable("Marketo"), +// } +// +// The specifications can be found at https://segment.com/docs/spec/common/#integrations +type Integrations map[string]interface{} + +func NewIntegrations() Integrations { + return make(Integrations, 10) +} + +func (i Integrations) EnableAll() Integrations { + return i.Enable("all") +} + +func (i Integrations) DisableAll() Integrations { + return i.Disable("all") +} + +func (i Integrations) Enable(name string) Integrations { + return i.Set(name, true) +} + +func (i Integrations) Disable(name string) Integrations { + return i.Set(name, false) +} + +// Sets an integration named by the first argument to the specified value, any +// value other than `false` will be interpreted as enabling the integration. +func (i Integrations) Set(name string, value interface{}) Integrations { + i[name] = value + return i +} diff --git a/vendor/github.com/rudderlabs/analytics-go/json.go b/vendor/github.com/rudderlabs/analytics-go/json.go new file mode 100644 index 0000000000..cd3b1752ef --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/json.go @@ -0,0 +1,87 @@ +package analytics + +import ( + "reflect" + "strings" +) + +// Imitate what what the JSON package would do when serializing a struct value, +// the only difference is we we don't serialize zero-value struct fields as well. +// Note that this function doesn't recursively convert structures to maps, only +// the value passed as argument is transformed. +func structToMap(v reflect.Value, m map[string]interface{}) map[string]interface{} { + t := v.Type() + n := t.NumField() + + if m == nil { + m = make(map[string]interface{}, n) + } + + for i := 0; i != n; i++ { + field := t.Field(i) + value := v.Field(i) + name, omitempty := parseJsonTag(field.Tag.Get("json"), field.Name) + + if name != "-" && !(omitempty && isZeroValue(value)) { + m[name] = value.Interface() + } + } + + return m +} + +// Parses a JSON tag the way the json package would do it, returing the expected +// name of the field once serialized and if empty values should be omitted. +func parseJsonTag(tag string, defName string) (name string, omitempty bool) { + args := strings.Split(tag, ",") + + if len(args) == 0 || len(args[0]) == 0 { + name = defName + } else { + name = args[0] + } + + if len(args) > 1 && args[1] == "omitempty" { + omitempty = true + } + + return +} + +// Checks if the value given as argument is a zero-value, it is based on the +// isEmptyValue function in https://golang.org/src/encoding/json/encode.go +// but also checks struct types recursively. +func isZeroValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + + case reflect.Bool: + return !v.Bool() + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + + case reflect.Interface, reflect.Ptr: + return v.IsNil() + + case reflect.Struct: + for i, n := 0, v.NumField(); i != n; i++ { + if !isZeroValue(v.Field(i)) { + return false + } + } + return true + + case reflect.Invalid: + return true + } + + return false +} diff --git a/vendor/github.com/rudderlabs/analytics-go/logger.go b/vendor/github.com/rudderlabs/analytics-go/logger.go new file mode 100644 index 0000000000..54190c5f39 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/logger.go @@ -0,0 +1,47 @@ +package analytics + +import ( + "log" + "os" +) + +// Instances of types implementing this interface can be used to define where +// the analytics client logs are written. +type Logger interface { + + // Analytics clients call this method to log regular messages about the + // operations they perform. + // Messages logged by this method are usually tagged with an `INFO` log + // level in common logging libraries. + Logf(format string, args ...interface{}) + + // Analytics clients call this method to log errors they encounter while + // sending events to the backend servers. + // Messages logged by this method are usually tagged with an `ERROR` log + // level in common logging libraries. + Errorf(format string, args ...interface{}) +} + +// This function instantiate an object that statisfies the analytics.Logger +// interface and send logs to standard logger passed as argument. +func StdLogger(logger *log.Logger) Logger { + return stdLogger{ + logger: logger, + } +} + +type stdLogger struct { + logger *log.Logger +} + +func (l stdLogger) Logf(format string, args ...interface{}) { + l.logger.Printf("INFO: "+format, args...) +} + +func (l stdLogger) Errorf(format string, args ...interface{}) { + l.logger.Printf("ERROR: "+format, args...) +} + +func newDefaultLogger() Logger { + return StdLogger(log.New(os.Stderr, "segment ", log.LstdFlags)) +} diff --git a/vendor/github.com/rudderlabs/analytics-go/message.go b/vendor/github.com/rudderlabs/analytics-go/message.go new file mode 100644 index 0000000000..a4999c6458 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/message.go @@ -0,0 +1,131 @@ +package analytics + +import ( + "encoding/json" + "time" +) + +// Values implementing this interface are used by analytics clients to notify +// the application when a message send succeeded or failed. +// +// Callback methods are called by a client's internal goroutines, there are no +// guarantees on which goroutine will trigger the callbacks, the calls can be +// made sequentially or in parallel, the order doesn't depend on the order of +// messages were queued to the client. +// +// Callback methods must return quickly and not cause long blocking operations +// to avoid interferring with the client's internal work flow. +type Callback interface { + + // This method is called for every message that was successfully sent to + // the API. + Success(Message) + + // This method is called for every message that failed to be sent to the + // API and will be discarded by the client. + Failure(Message, error) +} + +// This interface is used to represent analytics objects that can be sent via +// a client. +// +// Types like analytics.Track, analytics.Page, etc... implement this interface +// and therefore can be passed to the analytics.Client.Send method. +type Message interface { + + // Validate validates the internal structure of the message, the method must return + // nil if the message is valid, or an error describing what went wrong. + Validate() error + + // internal is an unexposed interface function to ensure only types defined within this package can satisfy the Message interface. Invoking this method will panic. + internal() +} + +// Takes a message id as first argument and returns it, unless it's the zero- +// value, in that case the default id passed as second argument is returned. +func makeMessageId(id string, def string) string { + if len(id) == 0 { + return def + } + return id +} + +// Returns the time value passed as first argument, unless it's the zero-value, +// in that case the default value passed as second argument is returned. +func makeTimestamp(t time.Time, def time.Time) time.Time { + if t == (time.Time{}) { + return def + } + return t +} + +// This structure represents objects sent to the /v1/batch endpoint. We don't +// export this type because it's only meant to be used internally to send groups +// of messages in one API call. +type batch struct { + MessageId string `json:"messageId"` + SentAt time.Time `json:"sentAt"` + Messages []message `json:"batch"` + Context *Context `json:"context"` +} + +type message struct { + msg Message + json []byte +} + +func makeMessage(m Message, maxBytes int) (msg message, err error) { + if msg.json, err = json.Marshal(m); err == nil { + if len(msg.json) > maxBytes { + err = ErrMessageTooBig + } else { + msg.msg = m + } + } + return +} + +func (m message) MarshalJSON() ([]byte, error) { + return m.json, nil +} + +func (m message) size() int { + // The `+ 1` is for the comma that sits between each items of a JSON array. + return len(m.json) + 1 +} + +type messageQueue struct { + pending []message + bytes int + maxBatchSize int + maxBatchBytes int +} + +func (q *messageQueue) push(m message) (b []message) { + if (q.bytes + m.size()) > q.maxBatchBytes { + b = q.flush() + } + + if q.pending == nil { + q.pending = make([]message, 0, q.maxBatchSize) + } + + q.pending = append(q.pending, m) + q.bytes += len(m.json) + + if b == nil && len(q.pending) == q.maxBatchSize { + b = q.flush() + } + + return +} + +func (q *messageQueue) flush() (msgs []message) { + msgs, q.pending, q.bytes = q.pending, nil, 0 + return +} + +const ( + maxBatchBytes = 500000 + maxMessageBytes = 32000 +) diff --git a/vendor/github.com/rudderlabs/analytics-go/page.go b/vendor/github.com/rudderlabs/analytics-go/page.go new file mode 100644 index 0000000000..5a3b231f54 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/page.go @@ -0,0 +1,38 @@ +package analytics + +import "time" + +var _ Message = (*Page)(nil) + +// This type represents object sent in a page call as described in +// https://segment.com/docs/libraries/http/#page +type Page struct { + // This field is exported for serialization purposes and shouldn't be set by + // the application, its value is always overwritten by the library. + Type string `json:"type,omitempty"` + + MessageId string `json:"messageId,omitempty"` + AnonymousId string `json:"anonymousId,omitempty"` + UserId string `json:"userId,omitempty"` + Name string `json:"name,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` + Context *Context `json:"context,omitempty"` + Properties Properties `json:"properties,omitempty"` + Integrations Integrations `json:"integrations,omitempty"` +} + +func (msg Page) internal() { + panic(unimplementedError) +} + +func (msg Page) Validate() error { + if len(msg.UserId) == 0 && len(msg.AnonymousId) == 0 { + return FieldError{ + Type: "analytics.Page", + Name: "UserId", + Value: msg.UserId, + } + } + + return nil +} diff --git a/vendor/github.com/rudderlabs/analytics-go/properties.go b/vendor/github.com/rudderlabs/analytics-go/properties.go new file mode 100644 index 0000000000..8b218aebdc --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/properties.go @@ -0,0 +1,117 @@ +package analytics + +// This type is used to represent properties in messages that support it. +// It is a free-form object so the application can set any value it sees fit but +// a few helper method are defined to make it easier to instantiate properties with +// common fields. +// Here's a quick example of how this type is meant to be used: +// +// analytics.Page{ +// UserId: "0123456789", +// Properties: analytics.NewProperties() +// .SetRevenue(10.0) +// .SetCurrency("USD"), +// } +// +type Properties map[string]interface{} + +func NewProperties() Properties { + return make(Properties, 10) +} + +func (p Properties) SetRevenue(revenue float64) Properties { + return p.Set("revenue", revenue) +} + +func (p Properties) SetCurrency(currency string) Properties { + return p.Set("currency", currency) +} + +func (p Properties) SetValue(value float64) Properties { + return p.Set("value", value) +} + +func (p Properties) SetPath(path string) Properties { + return p.Set("path", path) +} + +func (p Properties) SetReferrer(referrer string) Properties { + return p.Set("referrer", referrer) +} + +func (p Properties) SetTitle(title string) Properties { + return p.Set("title", title) +} + +func (p Properties) SetURL(url string) Properties { + return p.Set("url", url) +} + +func (p Properties) SetName(name string) Properties { + return p.Set("name", name) +} + +func (p Properties) SetCategory(category string) Properties { + return p.Set("category", category) +} + +func (p Properties) SetSKU(sku string) Properties { + return p.Set("sku", sku) +} + +func (p Properties) SetPrice(price float64) Properties { + return p.Set("price", price) +} + +func (p Properties) SetProductId(id string) Properties { + return p.Set("id", id) +} + +func (p Properties) SetOrderId(id string) Properties { + return p.Set("orderId", id) +} + +func (p Properties) SetTotal(total float64) Properties { + return p.Set("total", total) +} + +func (p Properties) SetSubtotal(subtotal float64) Properties { + return p.Set("subtotal", subtotal) +} + +func (p Properties) SetShipping(shipping float64) Properties { + return p.Set("shipping", shipping) +} + +func (p Properties) SetTax(tax float64) Properties { + return p.Set("tax", tax) +} + +func (p Properties) SetDiscount(discount float64) Properties { + return p.Set("discount", discount) +} + +func (p Properties) SetCoupon(coupon string) Properties { + return p.Set("coupon", coupon) +} + +func (p Properties) SetProducts(products ...Product) Properties { + return p.Set("products", products) +} + +func (p Properties) SetRepeat(repeat bool) Properties { + return p.Set("repeat", repeat) +} + +func (p Properties) Set(name string, value interface{}) Properties { + p[name] = value + return p +} + +// This type represents products in the E-commerce API. +type Product struct { + ID string `json:"id,omitempty"` + SKU string `json:"sky,omitempty"` + Name string `json:"name,omitempty"` + Price float64 `json:"price"` +} diff --git a/vendor/github.com/rudderlabs/analytics-go/screen.go b/vendor/github.com/rudderlabs/analytics-go/screen.go new file mode 100644 index 0000000000..e810e79daa --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/screen.go @@ -0,0 +1,38 @@ +package analytics + +import "time" + +var _ Message = (*Screen)(nil) + +// This type represents object sent in a screen call as described in +// https://segment.com/docs/libraries/http/#screen +type Screen struct { + // This field is exported for serialization purposes and shouldn't be set by + // the application, its value is always overwritten by the library. + Type string `json:"type,omitempty"` + + MessageId string `json:"messageId,omitempty"` + AnonymousId string `json:"anonymousId,omitempty"` + UserId string `json:"userId,omitempty"` + Name string `json:"name,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` + Context *Context `json:"context,omitempty"` + Properties Properties `json:"properties,omitempty"` + Integrations Integrations `json:"integrations,omitempty"` +} + +func (msg Screen) internal() { + panic(unimplementedError) +} + +func (msg Screen) Validate() error { + if len(msg.UserId) == 0 && len(msg.AnonymousId) == 0 { + return FieldError{ + Type: "analytics.Screen", + Name: "UserId", + Value: msg.UserId, + } + } + + return nil +} diff --git a/vendor/github.com/rudderlabs/analytics-go/timeout_15.go b/vendor/github.com/rudderlabs/analytics-go/timeout_15.go new file mode 100644 index 0000000000..12b963eddb --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/timeout_15.go @@ -0,0 +1,16 @@ +// +build !go1.6 + +package analytics + +import "net/http" + +// http clients on versions of go before 1.6 only support timeout if the +// transport implements the `CancelRequest` method. +func supportsTimeout(transport http.RoundTripper) bool { + _, ok := transport.(requestCanceler) + return ok +} + +type requestCanceler interface { + CancelRequest(*http.Request) +} diff --git a/vendor/github.com/rudderlabs/analytics-go/timeout_16.go b/vendor/github.com/rudderlabs/analytics-go/timeout_16.go new file mode 100644 index 0000000000..1115cafb77 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/timeout_16.go @@ -0,0 +1,10 @@ +// +build go1.6 + +package analytics + +import "net/http" + +// http clients on versions of go after 1.6 always support timeout. +func supportsTimeout(transport http.RoundTripper) bool { + return true +} diff --git a/vendor/github.com/rudderlabs/analytics-go/track.go b/vendor/github.com/rudderlabs/analytics-go/track.go new file mode 100644 index 0000000000..9c92a809a5 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/track.go @@ -0,0 +1,46 @@ +package analytics + +import "time" + +var _ Message = (*Track)(nil) + +// This type represents object sent in a track call as described in +// https://segment.com/docs/libraries/http/#track +type Track struct { + // This field is exported for serialization purposes and shouldn't be set by + // the application, its value is always overwritten by the library. + Type string `json:"type,omitempty"` + + MessageId string `json:"messageId,omitempty"` + AnonymousId string `json:"anonymousId,omitempty"` + UserId string `json:"userId,omitempty"` + Event string `json:"event"` + Timestamp time.Time `json:"timestamp,omitempty"` + Context *Context `json:"context,omitempty"` + Properties Properties `json:"properties,omitempty"` + Integrations Integrations `json:"integrations,omitempty"` +} + +func (msg Track) internal() { + panic(unimplementedError) +} + +func (msg Track) Validate() error { + if len(msg.Event) == 0 { + return FieldError{ + Type: "analytics.Track", + Name: "Event", + Value: msg.Event, + } + } + + if len(msg.UserId) == 0 && len(msg.AnonymousId) == 0 { + return FieldError{ + Type: "analytics.Track", + Name: "UserId", + Value: msg.UserId, + } + } + + return nil +} diff --git a/vendor/github.com/rudderlabs/analytics-go/traits.go b/vendor/github.com/rudderlabs/analytics-go/traits.go new file mode 100644 index 0000000000..d4e82f07a7 --- /dev/null +++ b/vendor/github.com/rudderlabs/analytics-go/traits.go @@ -0,0 +1,89 @@ +package analytics + +import "time" + +// This type is used to represent traits in messages that support it. +// It is a free-form object so the application can set any value it sees fit but +// a few helper method are defined to make it easier to instantiate traits with +// common fields. +// Here's a quick example of how this type is meant to be used: +// +// analytics.Identify{ +// UserId: "0123456789", +// Traits: analytics.NewTraits() +// .SetFirstName("Luke") +// .SetLastName("Skywalker") +// .Set("Role", "Jedi"), +// } +// +// The specifications can be found at https://segment.com/docs/spec/identify/#traits +type Traits map[string]interface{} + +func NewTraits() Traits { + return make(Traits, 10) +} + +func (t Traits) SetAddress(address string) Traits { + return t.Set("address", address) +} + +func (t Traits) SetAge(age int) Traits { + return t.Set("age", age) +} + +func (t Traits) SetAvatar(url string) Traits { + return t.Set("avatar", url) +} + +func (t Traits) SetBirthday(date time.Time) Traits { + return t.Set("birthday", date) +} + +func (t Traits) SetCreatedAt(date time.Time) Traits { + return t.Set("createdAt", date) +} + +func (t Traits) SetDescription(desc string) Traits { + return t.Set("description", desc) +} + +func (t Traits) SetEmail(email string) Traits { + return t.Set("email", email) +} + +func (t Traits) SetFirstName(firstName string) Traits { + return t.Set("firstName", firstName) +} + +func (t Traits) SetGender(gender string) Traits { + return t.Set("gender", gender) +} + +func (t Traits) SetLastName(lastName string) Traits { + return t.Set("lastName", lastName) +} + +func (t Traits) SetName(name string) Traits { + return t.Set("name", name) +} + +func (t Traits) SetPhone(phone string) Traits { + return t.Set("phone", phone) +} + +func (t Traits) SetTitle(title string) Traits { + return t.Set("title", title) +} + +func (t Traits) SetUsername(username string) Traits { + return t.Set("username", username) +} + +func (t Traits) SetWebsite(url string) Traits { + return t.Set("website", url) +} + +func (t Traits) Set(field string, value interface{}) Traits { + t[field] = value + return t +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 37a705c089..e43367b6e1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -289,6 +289,9 @@ github.com/prometheus/procfs/internal/util # github.com/rs/cors v1.7.0 ## explicit github.com/rs/cors +# github.com/rudderlabs/analytics-go v3.1.0+incompatible +## explicit +github.com/rudderlabs/analytics-go # github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 ## explicit github.com/russellhaering/goxmldsig diff --git a/web/handlers.go b/web/handlers.go index e3cf169059..9a14ebd0c8 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -152,7 +152,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Frame-Options", "SAMEORIGIN") // Set content security policy. This is also specified in the root.html of the webapp in a meta tag. w.Header().Set("Content-Security-Policy", fmt.Sprintf( - "frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/%s", + "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com cdn.segment.com/analytics.js/%s", h.cspShaDirective, )) } else { diff --git a/web/handlers_test.go b/web/handlers_test.go index aac1244fdc..670b5439b1 100644 --- a/web/handlers_test.go +++ b/web/handlers_test.go @@ -296,7 +296,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/"}) + assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com cdn.segment.com/analytics.js/"}) }) t.Run("static, with subpath", func(t *testing.T) { @@ -333,7 +333,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/"}) + assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com cdn.segment.com/analytics.js/"}) // TODO: It's hard to unit test this now that the CSP directive is effectively // decided in Setup(). Circle back to this in master once the memory store is @@ -348,7 +348,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response = httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/"}) + assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com cdn.segment.com/analytics.js/"}) // TODO: See above. // assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/ 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed") })