mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-12389 Update segment library to v3 (#11472)
* Update analytics-go dependency * Migrate from segment library v2 to v3 * Reinclude gorilla/handlers module * Fix missing module * Remove reference to outdated analytics module
This commit is contained in:
committed by
Christopher Speller
parent
c9e289f828
commit
d844c52f06
3
Makefile
3
Makefile
@@ -576,9 +576,6 @@ update-dependencies: ## Uses go get -u to update all the dependencies while hold
|
||||
# Update all dependencies (does not update across major versions)
|
||||
go get -u
|
||||
|
||||
# Keep back because of breaking API changes
|
||||
go get -u github.com/segmentio/analytics-go@2.1.1
|
||||
|
||||
# Tidy up
|
||||
go mod tidy
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func (a *App) sendDailyDiagnostics(override bool) {
|
||||
}
|
||||
|
||||
func (a *App) SendDiagnostic(event string, properties map[string]interface{}) {
|
||||
a.Srv.diagnosticClient.Track(&analytics.Track{
|
||||
a.Srv.diagnosticClient.Enqueue(&analytics.Track{
|
||||
Event: event,
|
||||
UserId: a.DiagnosticId(),
|
||||
Properties: properties,
|
||||
|
||||
@@ -103,7 +103,7 @@ type Server struct {
|
||||
limitedClientConfig map[string]string
|
||||
|
||||
diagnosticId string
|
||||
diagnosticClient *analytics.Client
|
||||
diagnosticClient analytics.Client
|
||||
|
||||
phase2PermissionsMigrationComplete bool
|
||||
|
||||
@@ -745,15 +745,16 @@ func (s *Server) StartElasticsearch() {
|
||||
|
||||
func (s *Server) initDiagnostics(endpoint string) {
|
||||
if s.diagnosticClient == nil {
|
||||
client := analytics.New(SEGMENT_KEY)
|
||||
client.Logger = s.Log.StdLog(mlog.String("source", "segment"))
|
||||
config := analytics.Config{}
|
||||
config.Logger = analytics.StdLogger(s.Log.StdLog(mlog.String("source", "segment")))
|
||||
// For testing
|
||||
if endpoint != "" {
|
||||
client.Endpoint = endpoint
|
||||
client.Verbose = true
|
||||
client.Size = 1
|
||||
config.Endpoint = endpoint
|
||||
config.Verbose = true
|
||||
config.BatchSize = 1
|
||||
}
|
||||
client.Identify(&analytics.Identify{
|
||||
client, _ := analytics.NewWithConfig(SEGMENT_KEY, config)
|
||||
client.Enqueue(&analytics.Identify{
|
||||
UserId: s.diagnosticId,
|
||||
})
|
||||
|
||||
@@ -761,7 +762,7 @@ func (s *Server) initDiagnostics(endpoint string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ShutdownDiagnostics closes the diagnostic client.
|
||||
// shutdownDiagnostics closes the diagnostic client.
|
||||
func (s *Server) shutdownDiagnostics() error {
|
||||
if s.diagnosticClient != nil {
|
||||
return s.diagnosticClient.Close()
|
||||
|
||||
3
go.mod
3
go.mod
@@ -35,7 +35,6 @@ require (
|
||||
github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20190311042500-a93a6c6ea053
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/lib/pq v1.0.0
|
||||
@@ -61,7 +60,7 @@ require (
|
||||
github.com/prometheus/procfs v0.0.0-20190322151404-55ae3d9d5573 // indirect
|
||||
github.com/rs/cors v1.6.0
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
|
||||
github.com/segmentio/analytics-go v2.0.1-0.20160426181448-2d840d861c32+incompatible
|
||||
github.com/segmentio/analytics-go v3.0.1+incompatible
|
||||
github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c // indirect
|
||||
github.com/sirupsen/logrus v1.4.0
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -171,8 +171,6 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jaytaylor/html2text v0.0.0-20190311042500-a93a6c6ea053 h1:vAR93++rxlMlJRMK0hKD3l5La7FjpmUIxO1jnJmgTbI=
|
||||
github.com/jaytaylor/html2text v0.0.0-20190311042500-a93a6c6ea053/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4=
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
@@ -304,8 +302,8 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo
|
||||
github.com/satori/go.uuid v0.0.0-20180103174451-36e9d2ebbde5/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/segmentio/analytics-go v2.0.1-0.20160426181448-2d840d861c32+incompatible h1:VcfKSKy3OxvnkQzsZPD1170acL2qldNDZCiRJ7CzZwE=
|
||||
github.com/segmentio/analytics-go v2.0.1-0.20160426181448-2d840d861c32+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
|
||||
github.com/segmentio/analytics-go v3.0.1+incompatible h1:W7T3ieNQjPFMb+SE8SAVYo6mPkKK/Y37wYdiNf5lCVg=
|
||||
github.com/segmentio/analytics-go v3.0.1+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
|
||||
github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c h1:rsRTAcCR5CeNLkvgBVSjQoDGRRt6kggsE6XYBqCv2KQ=
|
||||
github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
|
||||
26
vendor/github.com/jehiah/go-strftime/README.md
generated
vendored
26
vendor/github.com/jehiah/go-strftime/README.md
generated
vendored
@@ -1,26 +0,0 @@
|
||||
go-strftime
|
||||
===========
|
||||
|
||||
go implementation of strftime
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
strftime "github.com/jehiah/go-strftime"
|
||||
)
|
||||
|
||||
func main() {
|
||||
t := time.Unix(1340244776, 0)
|
||||
utc, _ := time.LoadLocation("UTC")
|
||||
t = t.In(utc)
|
||||
fmt.Println(strftime.Format("%Y-%m-%d %H:%M:%S", t))
|
||||
// Output:
|
||||
// 2012-06-21 02:12:56
|
||||
}
|
||||
```
|
||||
72
vendor/github.com/jehiah/go-strftime/strftime.go
generated
vendored
72
vendor/github.com/jehiah/go-strftime/strftime.go
generated
vendored
@@ -1,72 +0,0 @@
|
||||
// go implementation of strftime
|
||||
package strftime
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// taken from time/format.go
|
||||
var conversion = map[rune]string{
|
||||
/*stdLongMonth */ 'B': "January",
|
||||
/*stdMonth */ 'b': "Jan",
|
||||
// stdNumMonth */ 'm': "1",
|
||||
/*stdZeroMonth */ 'm': "01",
|
||||
/*stdLongWeekDay */ 'A': "Monday",
|
||||
/*stdWeekDay */ 'a': "Mon",
|
||||
// stdDay */ 'd': "2",
|
||||
// stdUnderDay */ 'd': "_2",
|
||||
/*stdZeroDay */ 'd': "02",
|
||||
/*stdHour */ 'H': "15",
|
||||
// stdHour12 */ 'I': "3",
|
||||
/*stdZeroHour12 */ 'I': "03",
|
||||
// stdMinute */ 'M': "4",
|
||||
/*stdZeroMinute */ 'M': "04",
|
||||
// stdSecond */ 'S': "5",
|
||||
/*stdZeroSecond */ 'S': "05",
|
||||
/*stdLongYear */ 'Y': "2006",
|
||||
/*stdYear */ 'y': "06",
|
||||
/*stdPM */ 'p': "PM",
|
||||
// stdpm */ 'p': "pm",
|
||||
/*stdTZ */ 'Z': "MST",
|
||||
// stdISO8601TZ */ 'z': "Z0700", // prints Z for UTC
|
||||
// stdISO8601ColonTZ */ 'z': "Z07:00", // prints Z for UTC
|
||||
/*stdNumTZ */ 'z': "-0700", // always numeric
|
||||
// stdNumShortTZ */ 'b': "-07", // always numeric
|
||||
// stdNumColonTZ */ 'b': "-07:00", // always numeric
|
||||
/* nonStdMilli */ 'L': ".000",
|
||||
}
|
||||
|
||||
// This is an alternative to time.Format because no one knows
|
||||
// what date 040305 is supposed to create when used as a 'layout' string
|
||||
// this takes standard strftime format options. For a complete list
|
||||
// of format options see http://strftime.org/
|
||||
func Format(format string, t time.Time) string {
|
||||
retval := make([]byte, 0, len(format))
|
||||
for i, ni := 0, 0; i < len(format); i = ni + 2 {
|
||||
ni = strings.IndexByte(format[i:], '%')
|
||||
if ni < 0 {
|
||||
ni = len(format)
|
||||
} else {
|
||||
ni += i
|
||||
}
|
||||
retval = append(retval, []byte(format[i:ni])...)
|
||||
if ni+1 < len(format) {
|
||||
c := format[ni+1]
|
||||
if c == '%' {
|
||||
retval = append(retval, '%')
|
||||
} else {
|
||||
if layoutCmd, ok := conversion[rune(c)]; ok {
|
||||
retval = append(retval, []byte(t.Format(layoutCmd))...)
|
||||
} else {
|
||||
retval = append(retval, '%', c)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ni < len(format) {
|
||||
retval = append(retval, '%')
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(retval)
|
||||
}
|
||||
@@ -20,3 +20,13 @@ _cgo_export.*
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
\#*
|
||||
.\#*
|
||||
|
||||
# Artifacts
|
||||
tmp/*
|
||||
6
vendor/github.com/segmentio/analytics-go/.gitmodules
generated
vendored
Normal file
6
vendor/github.com/segmentio/analytics-go/.gitmodules
generated
vendored
Normal file
@@ -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
|
||||
19
vendor/github.com/segmentio/analytics-go/History.md
generated
vendored
19
vendor/github.com/segmentio/analytics-go/History.md
generated
vendored
@@ -1,9 +1,20 @@
|
||||
|
||||
v2.1.1 / 2016-04-26
|
||||
v3.0.1 / 2018-10-02
|
||||
===================
|
||||
|
||||
* Fix blocking the goroutine when Close is the first call.
|
||||
* Fix blocking the goroutine when the message queue fills up.
|
||||
* 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
|
||||
===================
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Copyright (c) 2012 Jehiah Czebotar
|
||||
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
|
||||
@@ -7,13 +9,13 @@ 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 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.
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
33
vendor/github.com/segmentio/analytics-go/Makefile
generated
vendored
33
vendor/github.com/segmentio/analytics-go/Makefile
generated
vendored
@@ -1,10 +1,31 @@
|
||||
ifndef CIRCLE_ARTIFACTS
|
||||
CIRCLE_ARTIFACTS=tmp
|
||||
endif
|
||||
|
||||
bootstrap:
|
||||
.buildscript/bootstrap.sh
|
||||
|
||||
dependencies:
|
||||
@go get -v -t ./...
|
||||
|
||||
vet:
|
||||
@godep go vet ./...
|
||||
@go vet ./...
|
||||
|
||||
build:
|
||||
@godep go build
|
||||
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
|
||||
|
||||
test:
|
||||
@godep go test -race -cover ./...
|
||||
build: test
|
||||
@go build ./...
|
||||
|
||||
.PHONY: vet build test
|
||||
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'; fi
|
||||
|
||||
ci: dependencies test e2e
|
||||
|
||||
.PHONY: bootstrap dependencies vet test e2e ci
|
||||
|
||||
55
vendor/github.com/segmentio/analytics-go/Readme.md
generated
vendored
55
vendor/github.com/segmentio/analytics-go/Readme.md
generated
vendored
@@ -1,8 +1,55 @@
|
||||
# analytics-go
|
||||
# analytics-go [](https://circleci.com/gh/segmentio/analytics-go/tree/master) [](https://godoc.org/github.com/segmentio/analytics-go)
|
||||
|
||||
Segment analytics client for Go. For additional documentation
|
||||
visit [https://segment.com/docs/libraries/go](https://segment.com/docs/libraries/go/) or view the [godocs](http://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
|
||||
|
||||
MIT
|
||||
The library is released under the [MIT license](License.md).
|
||||
|
||||
38
vendor/github.com/segmentio/analytics-go/alias.go
generated
vendored
Normal file
38
vendor/github.com/segmentio/analytics-go/alias.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
package analytics
|
||||
|
||||
import "time"
|
||||
|
||||
// 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) 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
|
||||
}
|
||||
587
vendor/github.com/segmentio/analytics-go/analytics.go
generated
vendored
587
vendor/github.com/segmentio/analytics-go/analytics.go
generated
vendored
@@ -2,307 +2,248 @@ package analytics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jehiah/go-strftime"
|
||||
"github.com/segmentio/backo-go"
|
||||
"github.com/xtgo/uuid"
|
||||
)
|
||||
|
||||
// Version of the client.
|
||||
const Version = "2.1.0"
|
||||
const Version = "3.0.0"
|
||||
|
||||
// Endpoint for the Segment API.
|
||||
const Endpoint = "https://api.segment.io"
|
||||
// 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
|
||||
|
||||
// DefaultContext of message batches.
|
||||
var DefaultContext = map[string]interface{}{
|
||||
"library": map[string]interface{}{
|
||||
"name": "analytics-go",
|
||||
"version": Version,
|
||||
},
|
||||
// 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
|
||||
}
|
||||
|
||||
// Backoff policy.
|
||||
var Backo = backo.DefaultBacko()
|
||||
type client struct {
|
||||
Config
|
||||
key string
|
||||
|
||||
// Message interface.
|
||||
type message interface {
|
||||
setMessageId(string)
|
||||
setTimestamp(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
|
||||
|
||||
// Message fields common to all.
|
||||
type Message struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
MessageId string `json:"messageId,omitempty"`
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
SentAt string `json:"sentAt,omitempty"`
|
||||
}
|
||||
|
||||
// Batch message.
|
||||
type Batch struct {
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
Messages []interface{} `json:"batch"`
|
||||
Message
|
||||
}
|
||||
|
||||
// Identify message.
|
||||
type Identify struct {
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
Integrations map[string]interface{} `json:"integrations,omitempty"`
|
||||
Traits map[string]interface{} `json:"traits,omitempty"`
|
||||
AnonymousId string `json:"anonymousId,omitempty"`
|
||||
UserId string `json:"userId,omitempty"`
|
||||
Message
|
||||
}
|
||||
|
||||
// Group message.
|
||||
type Group struct {
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
Integrations map[string]interface{} `json:"integrations,omitempty"`
|
||||
Traits map[string]interface{} `json:"traits,omitempty"`
|
||||
AnonymousId string `json:"anonymousId,omitempty"`
|
||||
UserId string `json:"userId,omitempty"`
|
||||
GroupId string `json:"groupId"`
|
||||
Message
|
||||
}
|
||||
|
||||
// Track message.
|
||||
type Track struct {
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
Integrations map[string]interface{} `json:"integrations,omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
AnonymousId string `json:"anonymousId,omitempty"`
|
||||
UserId string `json:"userId,omitempty"`
|
||||
Event string `json:"event"`
|
||||
Message
|
||||
}
|
||||
|
||||
// Page message.
|
||||
type Page struct {
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
Integrations map[string]interface{} `json:"integrations,omitempty"`
|
||||
Traits map[string]interface{} `json:"properties,omitempty"`
|
||||
AnonymousId string `json:"anonymousId,omitempty"`
|
||||
UserId string `json:"userId,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Message
|
||||
}
|
||||
|
||||
// Alias message.
|
||||
type Alias struct {
|
||||
PreviousId string `json:"previousId"`
|
||||
UserId string `json:"userId"`
|
||||
Message
|
||||
}
|
||||
|
||||
// Client which batches messages and flushes at the given Interval or
|
||||
// when the Size limit is exceeded. Set Verbose to true to enable
|
||||
// logging output.
|
||||
type Client struct {
|
||||
Endpoint string
|
||||
// Interval represents the duration at which messages are flushed. It may be
|
||||
// configured only before any messages are enqueued.
|
||||
Interval time.Duration
|
||||
Size int
|
||||
Logger *log.Logger
|
||||
Verbose bool
|
||||
Client http.Client
|
||||
key string
|
||||
msgs chan interface{}
|
||||
// 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{}
|
||||
uid func() string
|
||||
now func() time.Time
|
||||
once sync.Once
|
||||
wg sync.WaitGroup
|
||||
|
||||
// These synchronization primitives are used to control how many goroutines
|
||||
// are spawned by the client for uploads.
|
||||
upmtx sync.Mutex
|
||||
upcond sync.Cond
|
||||
upcount int
|
||||
// This HTTP client is used to send requests to the backend, it uses the
|
||||
// HTTP transport provided in the configuration.
|
||||
http http.Client
|
||||
}
|
||||
|
||||
// New client with write key.
|
||||
func New(key string) *Client {
|
||||
c := &Client{
|
||||
Endpoint: Endpoint,
|
||||
Interval: 5 * time.Second,
|
||||
Size: 250,
|
||||
Logger: log.New(os.Stderr, "segment ", log.LstdFlags),
|
||||
Verbose: false,
|
||||
Client: *http.DefaultClient,
|
||||
key: key,
|
||||
msgs: make(chan interface{}, 100),
|
||||
quit: make(chan struct{}),
|
||||
shutdown: make(chan struct{}),
|
||||
now: time.Now,
|
||||
uid: uid,
|
||||
}
|
||||
|
||||
c.upcond.L = &c.upmtx
|
||||
// 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
|
||||
}
|
||||
|
||||
// Alias buffers an "alias" message.
|
||||
func (c *Client) Alias(msg *Alias) error {
|
||||
if msg.UserId == "" {
|
||||
return errors.New("You must pass a 'userId'.")
|
||||
// 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
|
||||
}
|
||||
|
||||
if msg.PreviousId == "" {
|
||||
return errors.New("You must pass a 'previousId'.")
|
||||
c := &client{
|
||||
Config: makeConfig(config),
|
||||
key: writeKey,
|
||||
msgs: make(chan Message, 100),
|
||||
quit: make(chan struct{}),
|
||||
shutdown: make(chan struct{}),
|
||||
http: makeHttpClient(config.Transport),
|
||||
}
|
||||
|
||||
msg.Type = "alias"
|
||||
c.queue(msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Page buffers an "page" message.
|
||||
func (c *Client) Page(msg *Page) error {
|
||||
if msg.UserId == "" && msg.AnonymousId == "" {
|
||||
return errors.New("You must pass either an 'anonymousId' or 'userId'.")
|
||||
}
|
||||
|
||||
msg.Type = "page"
|
||||
c.queue(msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Group buffers an "group" message.
|
||||
func (c *Client) Group(msg *Group) error {
|
||||
if msg.GroupId == "" {
|
||||
return errors.New("You must pass a 'groupId'.")
|
||||
}
|
||||
|
||||
if msg.UserId == "" && msg.AnonymousId == "" {
|
||||
return errors.New("You must pass either an 'anonymousId' or 'userId'.")
|
||||
}
|
||||
|
||||
msg.Type = "group"
|
||||
c.queue(msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Identify buffers an "identify" message.
|
||||
func (c *Client) Identify(msg *Identify) error {
|
||||
if msg.UserId == "" && msg.AnonymousId == "" {
|
||||
return errors.New("You must pass either an 'anonymousId' or 'userId'.")
|
||||
}
|
||||
|
||||
msg.Type = "identify"
|
||||
c.queue(msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Track buffers an "track" message.
|
||||
func (c *Client) Track(msg *Track) error {
|
||||
if msg.Event == "" {
|
||||
return errors.New("You must pass 'event'.")
|
||||
}
|
||||
|
||||
if msg.UserId == "" && msg.AnonymousId == "" {
|
||||
return errors.New("You must pass either an 'anonymousId' or 'userId'.")
|
||||
}
|
||||
|
||||
msg.Type = "track"
|
||||
c.queue(msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) startLoop() {
|
||||
go c.loop()
|
||||
|
||||
cli = c
|
||||
return
|
||||
}
|
||||
|
||||
// Queue message.
|
||||
func (c *Client) queue(msg message) {
|
||||
c.once.Do(c.startLoop)
|
||||
msg.setMessageId(c.uid())
|
||||
msg.setTimestamp(timestamp(c.now()))
|
||||
func makeHttpClient(transport http.RoundTripper) http.Client {
|
||||
httpClient := http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
if supportsTimeout(transport) {
|
||||
httpClient.Timeout = 10 * time.Second
|
||||
}
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *client) Enqueue(msg Message) (err error) {
|
||||
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
|
||||
}
|
||||
|
||||
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() error {
|
||||
c.once.Do(c.startLoop)
|
||||
c.quit <- struct{}{}
|
||||
close(c.msgs)
|
||||
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 nil
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) sendAsync(msgs []interface{}) {
|
||||
c.upmtx.Lock()
|
||||
for c.upcount == 1000 {
|
||||
c.upcond.Wait()
|
||||
// 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)
|
||||
}
|
||||
c.upcount++
|
||||
c.upmtx.Unlock()
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
err := c.send(msgs)
|
||||
if err != nil {
|
||||
c.logf(err.Error())
|
||||
}
|
||||
c.upmtx.Lock()
|
||||
c.upcount--
|
||||
c.upcond.Signal()
|
||||
c.upmtx.Unlock()
|
||||
c.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// Send batch request.
|
||||
func (c *Client) send(msgs []interface{}) error {
|
||||
if len(msgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
func (c *client) send(msgs []message) {
|
||||
const attempts = 10
|
||||
|
||||
batch := new(Batch)
|
||||
batch.Messages = msgs
|
||||
batch.MessageId = c.uid()
|
||||
batch.SentAt = timestamp(c.now())
|
||||
batch.Context = DefaultContext
|
||||
b, err := json.Marshal(batch{
|
||||
MessageId: c.uid(),
|
||||
SentAt: c.now(),
|
||||
Messages: msgs,
|
||||
Context: c.DefaultContext,
|
||||
})
|
||||
|
||||
b, err := json.Marshal(batch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshalling msgs: %s", err)
|
||||
c.errorf("marshalling messages - %s", err)
|
||||
c.notifyFailure(msgs, err)
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := 0; i != attempts; i++ {
|
||||
if err = c.upload(b); err == nil {
|
||||
return 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
|
||||
}
|
||||
Backo.Sleep(i)
|
||||
}
|
||||
|
||||
return err
|
||||
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 {
|
||||
func (c *client) upload(b []byte) error {
|
||||
url := c.Endpoint + "/v1/batch"
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating request: %s", err)
|
||||
c.errorf("creating request - %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Add("User-Agent", "analytics-go (version: "+Version+")")
|
||||
@@ -310,98 +251,138 @@ func (c *Client) upload(b []byte) error {
|
||||
req.Header.Add("Content-Length", string(len(b)))
|
||||
req.SetBasicAuth(c.key, "")
|
||||
|
||||
res, err := c.Client.Do(req)
|
||||
res, err := c.http.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending request: %s", err)
|
||||
c.errorf("sending request - %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
return c.report(res)
|
||||
}
|
||||
|
||||
if res.StatusCode < 400 {
|
||||
c.verbose("response %s", res.Status)
|
||||
return nil
|
||||
// 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
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading response body: %s", err)
|
||||
if body, err = ioutil.ReadAll(res.Body); err != nil {
|
||||
c.errorf("response %d %s - %s", res.StatusCode, res.Status, err)
|
||||
return
|
||||
}
|
||||
|
||||
return fmt.Errorf("response %s: %d – %s", res.Status, res.StatusCode, string(body))
|
||||
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() {
|
||||
var msgs []interface{}
|
||||
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.verbose("buffer (%d/%d) %v", len(msgs), c.Size, msg)
|
||||
msgs = append(msgs, msg)
|
||||
if len(msgs) == c.Size {
|
||||
c.verbose("exceeded %d messages – flushing", c.Size)
|
||||
c.sendAsync(msgs)
|
||||
msgs = make([]interface{}, 0, c.Size)
|
||||
}
|
||||
c.push(&mq, msg, wg, ex)
|
||||
|
||||
case <-tick.C:
|
||||
if len(msgs) > 0 {
|
||||
c.verbose("interval reached - flushing %d", len(msgs))
|
||||
c.sendAsync(msgs)
|
||||
msgs = make([]interface{}, 0, c.Size)
|
||||
} else {
|
||||
c.verbose("interval reached – nothing to send")
|
||||
}
|
||||
c.flush(&mq, wg, ex)
|
||||
|
||||
case <-c.quit:
|
||||
tick.Stop()
|
||||
c.verbose("exit requested – draining msgs")
|
||||
// drain the msg channel.
|
||||
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.verbose("buffer (%d/%d) %v", len(msgs), c.Size, msg)
|
||||
msgs = append(msgs, msg)
|
||||
c.push(&mq, msg, wg, ex)
|
||||
}
|
||||
c.verbose("exit requested – flushing %d", len(msgs))
|
||||
c.sendAsync(msgs)
|
||||
c.wg.Wait()
|
||||
c.verbose("exit")
|
||||
c.shutdown <- struct{}{}
|
||||
|
||||
c.flush(&mq, wg, ex)
|
||||
c.debugf("exit")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verbose log.
|
||||
func (c *Client) verbose(msg string, args ...interface{}) {
|
||||
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.Logger.Printf(msg, args...)
|
||||
c.logf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Unconditional log.
|
||||
func (c *Client) logf(msg string, args ...interface{}) {
|
||||
c.Logger.Printf(msg, args...)
|
||||
func (c *client) logf(format string, args ...interface{}) {
|
||||
c.Logger.Logf(format, args...)
|
||||
}
|
||||
|
||||
// Set message timestamp if one is not already set.
|
||||
func (m *Message) setTimestamp(s string) {
|
||||
if m.Timestamp == "" {
|
||||
m.Timestamp = s
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set message id.
|
||||
func (m *Message) setMessageId(s string) {
|
||||
if m.MessageId == "" {
|
||||
m.MessageId = s
|
||||
func (c *client) notifyFailure(msgs []message, err error) {
|
||||
if c.Callback != nil {
|
||||
for _, m := range msgs {
|
||||
c.Callback.Failure(m.msg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return formatted timestamp.
|
||||
func timestamp(t time.Time) string {
|
||||
return strftime.Format("%Y-%m-%dT%H:%M:%S%z", t)
|
||||
}
|
||||
|
||||
// Return uuid string.
|
||||
func uid() string {
|
||||
return uuid.NewRandom().String()
|
||||
}
|
||||
|
||||
18
vendor/github.com/segmentio/analytics-go/circle.yml
generated
vendored
18
vendor/github.com/segmentio/analytics-go/circle.yml
generated
vendored
@@ -1,18 +0,0 @@
|
||||
machine:
|
||||
environment:
|
||||
IMPORT_PATH: "github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME"
|
||||
|
||||
dependencies:
|
||||
pre:
|
||||
- go get github.com/tools/godep
|
||||
|
||||
override:
|
||||
- mkdir -p "$GOPATH/src/$IMPORT_PATH"
|
||||
- rsync -azC --delete ./ "$GOPATH/src/$IMPORT_PATH/"
|
||||
|
||||
test:
|
||||
pre:
|
||||
- make vet
|
||||
|
||||
override:
|
||||
- make test
|
||||
173
vendor/github.com/segmentio/analytics-go/config.go
generated
vendored
Normal file
173
vendor/github.com/segmentio/analytics-go/config.go
generated
vendored
Normal file
@@ -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()
|
||||
}
|
||||
148
vendor/github.com/segmentio/analytics-go/context.go
generated
vendored
Normal file
148
vendor/github.com/segmentio/analytics-go/context.go
generated
vendored
Normal file
@@ -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))
|
||||
}
|
||||
60
vendor/github.com/segmentio/analytics-go/error.go
generated
vendored
Normal file
60
vendor/github.com/segmentio/analytics-go/error.go
generated
vendored
Normal file
@@ -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")
|
||||
)
|
||||
53
vendor/github.com/segmentio/analytics-go/executor.go
generated
vendored
Normal file
53
vendor/github.com/segmentio/analytics-go/executor.go
generated
vendored
Normal file
@@ -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()
|
||||
}
|
||||
40
vendor/github.com/segmentio/analytics-go/group.go
generated
vendored
Normal file
40
vendor/github.com/segmentio/analytics-go/group.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
package analytics
|
||||
|
||||
import "time"
|
||||
|
||||
// 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) 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
|
||||
}
|
||||
31
vendor/github.com/segmentio/analytics-go/identify.go
generated
vendored
Normal file
31
vendor/github.com/segmentio/analytics-go/identify.go
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package analytics
|
||||
|
||||
import "time"
|
||||
|
||||
// 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) validate() error {
|
||||
if len(msg.UserId) == 0 && len(msg.AnonymousId) == 0 {
|
||||
return FieldError{
|
||||
Type: "analytics.Identify",
|
||||
Name: "UserId",
|
||||
Value: msg.UserId,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
44
vendor/github.com/segmentio/analytics-go/integrations.go
generated
vendored
Normal file
44
vendor/github.com/segmentio/analytics-go/integrations.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
87
vendor/github.com/segmentio/analytics-go/json.go
generated
vendored
Normal file
87
vendor/github.com/segmentio/analytics-go/json.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
47
vendor/github.com/segmentio/analytics-go/logger.go
generated
vendored
Normal file
47
vendor/github.com/segmentio/analytics-go/logger.go
generated
vendored
Normal file
@@ -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))
|
||||
}
|
||||
128
vendor/github.com/segmentio/analytics-go/message.go
generated
vendored
Normal file
128
vendor/github.com/segmentio/analytics-go/message.go
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
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 {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 = 15000
|
||||
)
|
||||
32
vendor/github.com/segmentio/analytics-go/page.go
generated
vendored
Normal file
32
vendor/github.com/segmentio/analytics-go/page.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package analytics
|
||||
|
||||
import "time"
|
||||
|
||||
// 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) validate() error {
|
||||
if len(msg.UserId) == 0 && len(msg.AnonymousId) == 0 {
|
||||
return FieldError{
|
||||
Type: "analytics.Page",
|
||||
Name: "UserId",
|
||||
Value: msg.UserId,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
117
vendor/github.com/segmentio/analytics-go/properties.go
generated
vendored
Normal file
117
vendor/github.com/segmentio/analytics-go/properties.go
generated
vendored
Normal file
@@ -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"`
|
||||
}
|
||||
32
vendor/github.com/segmentio/analytics-go/screen.go
generated
vendored
Normal file
32
vendor/github.com/segmentio/analytics-go/screen.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package analytics
|
||||
|
||||
import "time"
|
||||
|
||||
// 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) validate() error {
|
||||
if len(msg.UserId) == 0 && len(msg.AnonymousId) == 0 {
|
||||
return FieldError{
|
||||
Type: "analytics.Screen",
|
||||
Name: "UserId",
|
||||
Value: msg.UserId,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
vendor/github.com/segmentio/analytics-go/timeout_15.go
generated
vendored
Normal file
16
vendor/github.com/segmentio/analytics-go/timeout_15.go
generated
vendored
Normal file
@@ -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)
|
||||
}
|
||||
10
vendor/github.com/segmentio/analytics-go/timeout_16.go
generated
vendored
Normal file
10
vendor/github.com/segmentio/analytics-go/timeout_16.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
40
vendor/github.com/segmentio/analytics-go/track.go
generated
vendored
Normal file
40
vendor/github.com/segmentio/analytics-go/track.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
package analytics
|
||||
|
||||
import "time"
|
||||
|
||||
// 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) 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
|
||||
}
|
||||
89
vendor/github.com/segmentio/analytics-go/traits.go
generated
vendored
Normal file
89
vendor/github.com/segmentio/analytics-go/traits.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
4
vendor/modules.txt
vendored
4
vendor/modules.txt
vendored
@@ -99,8 +99,6 @@ github.com/icrowley/fake
|
||||
github.com/inconshreveable/mousetrap
|
||||
# github.com/jaytaylor/html2text v0.0.0-20190311042500-a93a6c6ea053
|
||||
github.com/jaytaylor/html2text
|
||||
# github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869
|
||||
github.com/jehiah/go-strftime
|
||||
# github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/jmoiron/sqlx
|
||||
github.com/jmoiron/sqlx/reflectx
|
||||
@@ -195,7 +193,7 @@ github.com/rwcarlsen/goexif/exif
|
||||
github.com/rwcarlsen/goexif/tiff
|
||||
# github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529
|
||||
github.com/sean-/seed
|
||||
# github.com/segmentio/analytics-go v2.0.1-0.20160426181448-2d840d861c32+incompatible
|
||||
# github.com/segmentio/analytics-go v3.0.1+incompatible
|
||||
github.com/segmentio/analytics-go
|
||||
# github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c
|
||||
github.com/segmentio/backo-go
|
||||
|
||||
Reference in New Issue
Block a user