Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Add Transactions to sentry-go #235

Merged
merged 19 commits into from
Jun 3, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
return nil
}

if options.BeforeSend != nil {
if event.Type != transactionType && options.BeforeSend != nil {
h := &EventHint{}
if hint != nil {
h = hint
Expand Down
17 changes: 13 additions & 4 deletions dsn.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,8 @@ func (dsn Dsn) String() string {
return url
}

// StoreAPIURL returns assembled url to be used in the transport.
// It points to configures Sentry instance.
func (dsn Dsn) StoreAPIURL() *url.URL {
// GetAPIURL returns assembled url to be used in the transport.
func getAPIURL(dsn Dsn, s string) *url.URL {
var rawURL string
rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host)
if dsn.port != dsn.scheme.defaultPort() {
Expand All @@ -152,11 +151,21 @@ func (dsn Dsn) StoreAPIURL() *url.URL {
if dsn.path != "" {
rawURL += dsn.path
}
rawURL += fmt.Sprintf("/api/%d/store/", dsn.projectID)
rawURL += fmt.Sprintf("/api/%d/%v/", dsn.projectID, s)
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
parsedURL, _ := url.Parse(rawURL)
return parsedURL
}

// StoreAPIURL returns assembled url that points to store endpoint
func (dsn Dsn) StoreAPIURL() *url.URL {
return getAPIURL(dsn, "store")
}

// EnvelopeAPIURL returns assembled url that points to the envelope endpoint
func (dsn Dsn) EnvelopeAPIURL() *url.URL {
return getAPIURL(dsn, "envelope")
}

// RequestHeaders returns all the necessary headers that have to be used in the transport.
func (dsn Dsn) RequestHeaders() map[string]string {
auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+
Expand Down
21 changes: 15 additions & 6 deletions dsn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
)

type DsnTest struct {
in string
dsn *Dsn // expected value after parsing
url string // expected Store API URL
in string
dsn *Dsn // expected value after parsing
url string // expected Store API URL
envURL string // expected Envelope API URL
}

//nolint: gochecknoglobals
Expand All @@ -28,7 +29,8 @@ var dsnTests = map[string]DsnTest{
path: "/foo/bar",
projectID: 42,
},
url: "https://domain:8888/foo/bar/api/42/store/",
url: "https://domain:8888/foo/bar/api/42/store/",
envURL: "https://domain:8888/foo/bar/api/42/envelope/",
},
"MinimalSecure": {
in: "https://public@domain/42",
Expand All @@ -39,7 +41,8 @@ var dsnTests = map[string]DsnTest{
port: 443,
projectID: 42,
},
url: "https://domain/api/42/store/",
url: "https://domain/api/42/store/",
envURL: "https://domain/api/42/envelope/",
},
"MinimalInsecure": {
in: "http://public@domain/42",
Expand All @@ -50,7 +53,8 @@ var dsnTests = map[string]DsnTest{
port: 80,
projectID: 42,
},
url: "http://domain/api/42/store/",
url: "http://domain/api/42/store/",
envURL: "http://domain/api/42/envelope/",
},
}

Expand All @@ -71,6 +75,11 @@ func TestNewDsn(t *testing.T) {
if diff := cmp.Diff(tt.url, url); diff != "" {
t.Errorf("dsn.StoreAPIURL() mismatch (-want +got):\n%s", diff)
}
// Envelope API URL
url = dsn.EnvelopeAPIURL().String()
if diff := cmp.Diff(tt.envURL, url); diff != "" {
t.Errorf("dsn.EnvelopeAPIURL() mismatch (-want +got):\n%s", diff)
}
})
}
}
Expand Down
64 changes: 63 additions & 1 deletion interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import (
// Level marks the severity of the event
type Level string

// transactionType refers to an transaction event
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
const transactionType = "transaction"

const (
LevelDebug Level = "debug"
LevelInfo Level = "info"
Expand Down Expand Up @@ -143,8 +146,42 @@ type Exception struct {

type EventID string

// https://docs.sentry.io/development/sdk-dev/event-payloads/
// TraceContext describes the context of the trace.
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
type TraceContext struct {
TraceID string `json:"trace_id"`
SpanID string `json:"span_id"`
Op string `json:"op,omitempty"`
Description string `json:"description,omitempty"`
}

// Span describes a AM Span following the Sentry format.
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved
// Experimental: This is part of a beta feature of the SDK
type Span struct {
TraceID string `json:"trace_id"`
SpanID string `json:"span_id"`
ParentSpanID string `json:"parent_span_id,omitempty"`
Description string `json:"description,omitempty"`
Op string `json:"op,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
StartTimestamp time.Time `json:"start_timestamp"`
EndTimestamp time.Time `json:"timestamp"`
Status string `json:"status"`
}

// MarshalJSON converts the Span struct to JSON.
func (s *Span) MarshalJSON() ([]byte, error) {
type alias Span

return json.Marshal(&struct {
*alias
}{
alias: (*alias)(s),
})
}
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved

// Event is the fundamental data structure that is sent to Sentry
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
type Event struct {
Type string `json:"type,omitempty"`
Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"`
Contexts map[string]interface{} `json:"contexts,omitempty"`
Dist string `json:"dist,omitempty"`
Expand All @@ -167,15 +204,30 @@ type Event struct {
Modules map[string]string `json:"modules,omitempty"`
Request *Request `json:"request,omitempty"`
Exception []Exception `json:"exception,omitempty"`
// Experimental: This is part of a beta feature of the SDK
StartTimestamp time.Time `json:"start_timestamp"`
// Experimental: This is part of a beta feature of the SDK
Spans []*Span `json:"spans,omitempty"`
}

// MarshalJSON converts the Event struct to JSON.
func (e *Event) MarshalJSON() ([]byte, error) {
type alias Event
// encoding/json doesn't support the "omitempty" option for struct types.
// See https://golang.org/issues/11939.
// This implementation of MarshalJSON shadows the original Timestamp field
// forcing it to be omitted when the Timestamp is the zero value of
// time.Time.
if e.Timestamp.IsZero() && e.StartTimestamp.IsZero() {
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved
return json.Marshal(&struct {
*alias
Timestamp json.RawMessage `json:"timestamp,omitempty"`
StartTimestamp json.RawMessage `json:"start_timestamp,omitempty"`
}{
alias: (*alias)(e),
})
}

if e.Timestamp.IsZero() {
return json.Marshal(&struct {
*alias
Expand All @@ -184,6 +236,16 @@ func (e *Event) MarshalJSON() ([]byte, error) {
alias: (*alias)(e),
})
}

if e.StartTimestamp.IsZero() {
return json.Marshal(&struct {
*alias
StartTimestamp json.RawMessage `json:"start_timestamp,omitempty"`
}{
alias: (*alias)(e),
})
}

return json.Marshal(&struct {
*alias
}{
Expand Down
63 changes: 63 additions & 0 deletions interfaces_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package sentry

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"

"github.com/google/go-cmp/cmp"
)

var update = flag.Bool("update", false, "update .golden files") //nolint: gochecknoglobals

func TestNewRequest(t *testing.T) {
const payload = `{"test_data": true}`
got := NewRequest(httptest.NewRequest("POST", "/test/?q=sentry", strings.NewReader(payload)))
Expand All @@ -29,3 +38,57 @@ func TestNewRequest(t *testing.T) {
t.Errorf("Request mismatch (-want +got):\n%s", diff)
}
}

func TestMarshalStruct(t *testing.T) {
testSpan := &Span{
TraceID: "d6c4f03650bd47699ec65c84352b6208",
SpanID: "1cc4b26ab9094ef0",
ParentSpanID: "442bd97bbe564317",
Description: `SELECT * FROM user WHERE "user"."id" = {id}`,
Op: "db.sql",
Tags: map[string]string{
"function_name": "get_users",
"status_message": "MYSQL OK",
},
StartTimestamp: time.Unix(0, 0).UTC(),
EndTimestamp: time.Unix(5, 0).UTC(),
Status: "ok",
}

testCases := []struct {
testName string
sentryStruct interface{}
}{
{
testName: "span",
sentryStruct: testSpan,
},
}

for _, test := range testCases {
test := test
t.Run(test.testName, func(t *testing.T) {
got, err := json.MarshalIndent(test.sentryStruct, "", " ")
if err != nil {
t.Error(err)
}

golden := filepath.Join(".", "testdata", fmt.Sprintf("%s.golden", test.testName))
if *update {
err := ioutil.WriteFile(golden, got, 0600)
if err != nil {
t.Error(err)
}
}
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved

want, err := ioutil.ReadFile(golden)
if err != nil {
t.Error(err)
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
}

if !bytes.Equal(got, want) {
t.Errorf("struct %s\n\tgot:\n%s\n\twant:\n%s", test.testName, got, want)
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
}
})
}
}
14 changes: 14 additions & 0 deletions testdata/span.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"trace_id": "d6c4f03650bd47699ec65c84352b6208",
"span_id": "1cc4b26ab9094ef0",
"parent_span_id": "442bd97bbe564317",
"description": "SELECT * FROM user WHERE \"user\".\"id\" = {id}",
"op": "db.sql",
"tags": {
"function_name": "get_users",
"status_message": "MYSQL OK"
},
"start_timestamp": "1970-01-01T00:00:00Z",
"timestamp": "1970-01-01T00:00:05Z",
"status": "ok"
}
47 changes: 39 additions & 8 deletions transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
Expand Down Expand Up @@ -92,6 +94,41 @@ func getRequestBodyFromEvent(event *Event) []byte {
return nil
}

func getEnvelopeFromBody(body []byte) *bytes.Buffer {
var b bytes.Buffer
fmt.Fprintf(&b, `{"sent_at":"%s"}`, time.Now().UTC().Format(time.RFC3339Nano))
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprint(&b, "\n", `{"type":"transaction"}`, "\n")
b.Write(body)
b.WriteString("\n")
return &b
}

func getRequestFromEvent(event *Event, dsn *Dsn) (*http.Request, error) {
body := getRequestBodyFromEvent(event)
if body == nil {
return nil, errors.New("event could not be marshalled")
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved
}

if event.Type == transactionType {
env := getEnvelopeFromBody(body)
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
request, _ := http.NewRequest(
http.MethodPost,
dsn.EnvelopeAPIURL().String(),
env,
)

return request, nil
}

request, _ := http.NewRequest(
http.MethodPost,
dsn.StoreAPIURL().String(),
bytes.NewBuffer(body),
)

return request, nil
}

// ================================
// HTTPTransport
// ================================
Expand Down Expand Up @@ -187,17 +224,11 @@ func (t *HTTPTransport) SendEvent(event *Event) {
return
}

body := getRequestBodyFromEvent(event)
if body == nil {
request, err := getRequestFromEvent(event, t.dsn)
if err != nil {
return
}

request, _ := http.NewRequest(
http.MethodPost,
t.dsn.StoreAPIURL().String(),
bytes.NewBuffer(body),
)
Comment on lines -190 to -199
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two builtin transport implementations, we need this to be applied to HTTPSyncTransport too, so it becomes an alternative to send transactions/envelopes too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will address this in next PR


for headerKey, headerValue := range t.dsn.RequestHeaders() {
request.Header.Set(headerKey, headerValue)
}
Expand Down
Loading