Skip to content

Commit c0842f2

Browse files
committed
Add FromContext and Context.WithContext for lots of context
This allows building up a honeybadger.Context riding along with a context.Context so it can be sent with errors. The goal is to make it so that the context sent to Honeybadger is tied to the request (or stack) rather than multiple requests clobbering the global state (see #35). Fixes #35 Closes #37
1 parent 0e19fe6 commit c0842f2

File tree

9 files changed

+186
-7
lines changed

9 files changed

+186
-7
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ CHANGELOG](http://keepachangelog.com/) for how to update this file. This project
44
adheres to [Semantic Versioning](http://semver.org/).
55

66
## [Unreleased][unreleased]
7+
### Added
8+
- Added `honeybadger.FromContext` to retrieve a honeybadger.Context from a
9+
context.Context.
10+
- Added `Context.WithContext` for storing a honeybadger.Context into a
11+
context.Context.
712

813
### Changed
914
- Removed honeybadger.SetContext and client.SetContext (#35) -@gaffneyc

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,26 @@ honeybadger.Notify(err, honeybadger.Tags{"timeout", "http"})
155155

156156
---
157157

158+
When using Go's context.Context you can store a honeybadger.Context to build it
159+
up across multiple middleware. Be aware that honeybadger.Context is not thread
160+
safe.
161+
```go
162+
func(resp http.ResponseWriter, req *http.Request) {
163+
// To store a honeybadger.Context (or use honeybadger.Handler which does this for you)
164+
hbCtx := honeybadger.Context{}
165+
req = req.WithContext(hbCtx.WithContext(req.Context()))
166+
167+
// To add to an existing context
168+
hbCtx = honeybadger.FromContext(req.Context())
169+
hbCtx["user_id"] = "ID"
170+
171+
// To add the context when sending you can just pass the context.Context
172+
honeybadger.Notify(err, ctx)
173+
}
174+
```
175+
176+
---
177+
158178
### ``defer honeybadger.Monitor()``: Automatically report panics from your functions
159179

160180
To automatically report panics in your functions or methods, add

client.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,20 @@ func (client *Client) Handler(h http.Handler) http.Handler {
8181
if h == nil {
8282
h = http.DefaultServeMux
8383
}
84-
fn := func(w http.ResponseWriter, r *http.Request) {
84+
fn := func(w http.ResponseWriter, req *http.Request) {
8585
defer func() {
8686
if err := recover(); err != nil {
87-
client.Notify(newError(err, 2), Params(r.Form), getCGIData(r), *r.URL)
87+
client.Notify(newError(err, 2), Params(req.Form), getCGIData(req), *req.URL)
8888
panic(err)
8989
}
9090
}()
91-
h.ServeHTTP(w, r)
91+
92+
// Add a fresh Context to the request if one is not already set
93+
if hbCtx := FromContext(req.Context()); hbCtx == nil {
94+
req = req.WithContext(Context{}.WithContext(req.Context()))
95+
}
96+
97+
h.ServeHTTP(w, req)
9298
}
9399
return http.HandlerFunc(fn)
94100
}

client_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package honeybadger
22

33
import (
4+
"context"
5+
"fmt"
46
"testing"
57
)
68

@@ -27,3 +29,37 @@ func TestConfigureClientEndpoint(t *testing.T) {
2729
t.Errorf("Expected Configure to update backend. expected=%#v actual=%#v", "http://localhost:3000", backend.URL)
2830
}
2931
}
32+
33+
func TestClientContext(t *testing.T) {
34+
backend := NewMemoryBackend()
35+
36+
client := New(Configuration{
37+
APIKey: "badgers",
38+
Backend: backend,
39+
})
40+
41+
err := NewError(fmt.Errorf("which context is which"))
42+
43+
hbCtx := Context{"user_id": 1}
44+
goCtx := Context{"request_id": "1234"}.WithContext(context.Background())
45+
46+
_, nErr := client.Notify(err, hbCtx, goCtx)
47+
if nErr != nil {
48+
t.Fatal(nErr)
49+
}
50+
51+
// Flush otherwise backend.Notices will be empty
52+
client.Flush()
53+
54+
if len(backend.Notices) != 1 {
55+
t.Fatalf("Notices expected=%d actual=%d", 1, len(backend.Notices))
56+
}
57+
58+
notice := backend.Notices[0]
59+
if notice.Context["user_id"] != 1 {
60+
t.Errorf("notice.Context[user_id] expected=%d actual=%v", 1, notice.Context["user_id"])
61+
}
62+
if notice.Context["request_id"] != "1234" {
63+
t.Errorf("notice.Context[request_id] expected=%q actual=%v", "1234", notice.Context["request_id"])
64+
}
65+
}

context.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
package honeybadger
22

3+
import "context"
4+
35
// Context is used to send extra data to Honeybadger.
46
type Context hash
57

8+
// ctxKey is use in WithContext and FromContext to store and load the
9+
// honeybadger.Context into a context.Context.
10+
type ctxKey struct{}
11+
612
// Update applies the values in other Context to context.
7-
func (context Context) Update(other Context) {
13+
func (c Context) Update(other Context) {
814
for k, v := range other {
9-
context[k] = v
15+
c[k] = v
16+
}
17+
}
18+
19+
// WithContext adds the honeybadger.Context to the given context.Context and
20+
// returns the new context.Context.
21+
func (c Context) WithContext(ctx context.Context) context.Context {
22+
return context.WithValue(ctx, ctxKey{}, c)
23+
}
24+
25+
// FromContext retrieves a honeybadger.Context from the context.Context.
26+
// FromContext will return nil if no Honeybadger context exists in ctx.
27+
func FromContext(ctx context.Context) Context {
28+
if c, ok := ctx.Value(ctxKey{}).(Context); ok {
29+
return c
1030
}
31+
32+
return nil
1133
}

context_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package honeybadger
22

3-
import "testing"
3+
import (
4+
"context"
5+
"testing"
6+
)
47

58
func TestContextUpdate(t *testing.T) {
69
c := Context{"foo": "bar"}
@@ -9,3 +12,26 @@ func TestContextUpdate(t *testing.T) {
912
t.Errorf("Context should update values. expected=%#v actual=%#v", "baz", c["foo"])
1013
}
1114
}
15+
16+
func TestContext(t *testing.T) {
17+
t.Run("setting values is allowed between reads", func(t *testing.T) {
18+
ctx := context.Background()
19+
ctx = Context{"foo": "bar"}.WithContext(ctx)
20+
21+
stored := FromContext(ctx)
22+
if stored == nil {
23+
t.Fatalf("FromContext returned nil")
24+
}
25+
if stored["foo"] != "bar" {
26+
t.Errorf("stored[foo] expected=%q actual=%v", "bar", stored["foo"])
27+
}
28+
29+
// Write a new key then we'll read from the ctx again and make sure it is
30+
// still set.
31+
stored["baz"] = "qux"
32+
stored = FromContext(ctx)
33+
if stored["baz"] != "qux" {
34+
t.Errorf("stored[baz] expected=%q actual=%v", "qux", stored["baz"])
35+
}
36+
})
37+
}

memory_backend.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package honeybadger
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"sync"
7+
)
8+
9+
// MemoryBackend is a Backend that writes error notices to a slice. The
10+
// MemoryBackend is mainly useful for testing and will cause issues if used in
11+
// production. MemoryBackend is thread safe but order can't be guaranteed.
12+
type MemoryBackend struct {
13+
Notices []*Notice
14+
mu sync.Mutex
15+
}
16+
17+
// NewMemoryBackend creates a new MemoryBackend.
18+
func NewMemoryBackend() *MemoryBackend {
19+
return &MemoryBackend{
20+
Notices: make([]*Notice, 0),
21+
}
22+
}
23+
24+
// Notify adds the given payload (if it is a Notice) to Notices.
25+
func (b *MemoryBackend) Notify(_ Feature, payload Payload) error {
26+
notice, ok := payload.(*Notice)
27+
if !ok {
28+
return fmt.Errorf("memory backend does not support payload of type %q", reflect.TypeOf(payload))
29+
}
30+
31+
b.mu.Lock()
32+
defer b.mu.Unlock()
33+
34+
b.Notices = append(b.Notices, notice)
35+
36+
return nil
37+
}
38+
39+
// Reset clears the set of Notices
40+
func (b *MemoryBackend) Reset() {
41+
b.mu.Lock()
42+
defer b.mu.Unlock()
43+
44+
b.Notices = b.Notices[:0]
45+
}

notice.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package honeybadger
22

33
import (
4+
"context"
45
"encoding/json"
56
"net/url"
67
"os"
@@ -174,6 +175,11 @@ func newNotice(config *Configuration, err Error, extra ...interface{}) *Notice {
174175
notice.CGIData = t
175176
case url.URL:
176177
notice.URL = t.String()
178+
case context.Context:
179+
context := FromContext(t)
180+
if context != nil {
181+
notice.setContext(context)
182+
}
177183
}
178184
}
179185

notice_test.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package honeybadger
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"testing"
@@ -53,10 +54,22 @@ func TestNewNotice(t *testing.T) {
5354
t.Errorf("Expected notice not to trash project root. expected=%#v result=%#v", "/path/to/root/badgers.go", notice.Backtrace[0].File)
5455
}
5556

56-
notice = newNotice(&Configuration{}, err, Context{"foo":"bar"})
57+
notice = newNotice(&Configuration{}, err, Context{"foo": "bar"})
5758
if notice.Context["foo"] != "bar" {
5859
t.Errorf("Expected notice to contain context. expected=%#v result=%#v", "bar", notice.Context["foo"])
5960
}
61+
62+
// Notices can take the Context that is stored in a context.Context
63+
notice = newNotice(&Configuration{}, err, Context{"foo": "bar"}.WithContext(context.Background()))
64+
if notice.Context["foo"] != "bar" {
65+
t.Errorf("Expected notice to contain context. expected=%#v result=%#v", "bar", notice.Context["foo"])
66+
}
67+
68+
// Notices given a context.Context without a Context don't set notice.Context
69+
notice = newNotice(&Configuration{}, err, context.Background())
70+
if len(notice.Context) != 0 {
71+
t.Errorf("Expected notice to contain empty context. result=%#v", notice.Context)
72+
}
6073
}
6174

6275
func TestToJSON(t *testing.T) {

0 commit comments

Comments
 (0)