Merge pull request #14 from mkobetic/request_params

Populate the Environement and Parameters tab in errbit reports
diff --git a/airbrake.go b/airbrake.go
index 7019d6c..ab02d51 100644
--- a/airbrake.go
+++ b/airbrake.go
@@ -8,6 +8,7 @@
 	"net/http"
 	"os"
 	"reflect"
+	"regexp"
 	"runtime"
 	"sync"
 	"text/template"
@@ -19,6 +20,12 @@
 	Environment = "development"
 	Verbose     = false
 
+	// PrettyParams allows including request query/form parameters on the Environment tab
+	// which is more readable than the raw text of the Parameters tab (in Errbit).
+	// The param keys will be rendered as "?<param>" so they will sort together at the top of the tab.
+	PrettyParams = false
+
+	sensitive     = regexp.MustCompile(`password|token|secret|key`)
 	badResponse   = errors.New("Bad response")
 	apiKeyMissing = errors.New("Please set the airbrake.ApiKey before doing calls")
 	dunno         = []byte("???")
@@ -84,12 +91,12 @@
 	}()
 }
 
-func post(params map[string]interface{}) {
+func post(params map[string]interface{}) error {
 	buffer := bytes.NewBufferString("")
 
 	if err := tmpl.Execute(buffer, params); err != nil {
 		log.Printf("Airbrake error: %s", err)
-		return
+		return err
 	}
 
 	if Verbose {
@@ -99,7 +106,7 @@
 	response, err := http.Post(Endpoint, "text/xml", buffer)
 	if err != nil {
 		log.Printf("Airbrake error: %s", err)
-		return
+		return err
 	}
 
 	if Verbose {
@@ -112,6 +119,7 @@
 		log.Printf("Airbrake post: %s status code: %d", params["Error"], response.StatusCode)
 	}
 
+	return nil
 }
 
 func Error(e error, request *http.Request) error {
@@ -121,34 +129,7 @@
 		return apiKeyMissing
 	}
 
-	params := map[string]interface{}{
-		"Class":       reflect.TypeOf(e).String(),
-		"Error":       e,
-		"ApiKey":      ApiKey,
-		"ErrorName":   e.Error(),
-		"Environment": Environment,
-		"Request":     request,
-	}
-
-	if params["Class"] == "" {
-		params["Class"] = "Panic"
-	}
-
-	pwd, err := os.Getwd()
-	if err == nil {
-		params["Pwd"] = pwd
-	}
-
-	hostname, err := os.Hostname()
-	if err == nil {
-		params["Hostname"] = hostname
-	}
-
-	params["Backtrace"] = stacktrace(3)
-
-	post(params)
-
-	return nil
+	return post(params(e, request))
 }
 
 func Notify(e error) error {
@@ -158,6 +139,10 @@
 		return apiKeyMissing
 	}
 
+	return post(params(e, nil))
+}
+
+func params(e error, request *http.Request) map[string]interface{} {
 	params := map[string]interface{}{
 		"Class":       reflect.TypeOf(e).String(),
 		"Error":       e,
@@ -182,9 +167,51 @@
 
 	params["Backtrace"] = stacktrace(3)
 
-	post(params)
-	return nil
+	if request == nil || request.ParseForm() != nil {
+		return params
+	}
 
+	// Compile relevant request parameters into a map.
+	req := make(map[string]interface{})
+	params["Request"] = req
+	req["Component"] = ""
+	req["Action"] = ""
+	// Nested http Muxes muck with the URL, prefer RequestURI.
+	if request.RequestURI != "" {
+		req["URL"] = request.RequestURI
+	} else {
+		req["URL"] = request.URL
+	}
+
+	// Compile header parameters.
+	header := make(map[string]string)
+	req["Header"] = header
+	header["Method"] = request.Method
+	header["Protocol"] = request.Proto
+	for k, v := range request.Header {
+		if !omit(k, v) {
+			header[k] = v[0]
+		}
+	}
+
+	// Compile query/form parameters.
+	form := make(map[string]string)
+	req["Form"] = form
+	for k, v := range request.Form {
+		if !omit(k, v) {
+			form[k] = v[0]
+			if PrettyParams {
+				header["?"+k] = v[0]
+			}
+		}
+	}
+
+	return params
+}
+
+// omit checks the key, values for emptiness or sensitivity.
+func omit(key string, values []string) bool {
+	return len(key) == 0 || len(values) == 0 || len(values[0]) == 0 || sensitive.FindString(key) != ""
 }
 
 func CapturePanic(r *http.Request) {
@@ -212,22 +239,24 @@
   </notifier>
   <error>
     <class>{{ html .Class }}</class>
-    <message>{{ with .ErrorName }}{{html .}}{{ end }}</message>
-    <backtrace>
-      {{ range .Backtrace }}
-      <line method="{{ html .Function}}" file="{{ html .File}}" number="{{.Line}}"/>
-      {{ end }}
+    <message>{{ html .ErrorName }}</message>
+    <backtrace>{{ range .Backtrace }}
+      <line method="{{ html .Function}}" file="{{ html .File}}" number="{{.Line}}"/>{{ end }}
     </backtrace>
-  </error>
-  {{ with .Request }}
+  </error>{{ with .Request }}
   <request>
-    <url>{{ html .URL }}</url>
-    <component/>
-    <action/>
-  </request>
-  {{ end }}  
+    <url>{{html .URL}}</url>
+    <component>{{ .Component }}</component>
+    <action>{{ .Action }}</action>
+    <params>{{ range $key, $value := .Form }}
+      <var key="{{ $key }}">{{ $value }}</var>{{ end }}
+    </params>
+    <cgi-data>{{ range $key, $value := .Header }}
+      <var key="{{ $key }}">{{ $value }}</var>{{ end }}
+    </cgi-data>
+  </request>{{ end }}
   <server-environment>
-    <project-root>{{ html .Pwd }}</project-root>   
+    <project-root>{{ html .Pwd }}</project-root>
     <environment-name>{{ .Environment }}</environment-name>
     <hostname>{{ html .Hostname }}</hostname>
   </server-environment>
diff --git a/airbrake_test.go b/airbrake_test.go
index 10000d1..ddd3674 100644
--- a/airbrake_test.go
+++ b/airbrake_test.go
@@ -4,6 +4,7 @@
 	"bytes"
 	"errors"
 	"net/http"
+	"regexp"
 	"testing"
 	"time"
 )
@@ -44,12 +45,81 @@
 	Verbose = true
 	ApiKey = API_KEY
 	Endpoint = "https://api.airbrake.io/notifier_api/v2/notices"
-	
+
 	err := Notify(errors.New("Test Error"))
-	
+
 	if err != nil {
 		t.Error(err)
 	}
 
 	time.Sleep(1e9)
 }
+
+// Make sure we match https://help.airbrake.io/kb/api-2/notifier-api-version-23
+func TestTemplateV2(t *testing.T) {
+	var p map[string]interface{}
+	request, _ := http.NewRequest("GET", "/query?t=xxx&q=SHOW+x+BY+y+FROM+z&key=sesame&timezone=", nil)
+	request.Header.Set("Host", "Zulu")
+	request.Header.Set("Keep_Secret", "Sesame")
+	PrettyParams = true
+	defer func() { PrettyParams = false }()
+
+	// Trigger and recover a panic, so that we have something to render.
+	func() {
+		defer func() {
+			if r := recover(); r != nil {
+				p = params(r.(error), request)
+			}
+		}()
+		panic(errors.New("Boom!"))
+	}()
+
+	// Did that work?
+	if p == nil {
+		t.Fail()
+	}
+
+	// Crude backtrace check.
+	if len(p["Backtrace"].([]Line)) < 3 {
+		t.Fail()
+	}
+
+	// It's messy to generically test rendered backtrace, drop it.
+	delete(p, "Backtrace")
+
+	// Render the params.
+	var b bytes.Buffer
+	if err := tmpl.Execute(&b, p); err != nil {
+		t.Fatalf("Template error: %s", err)
+	}
+
+	// Validate the <error> node.
+	chunk := regexp.MustCompile(`(?s)<error>.*<backtrace>`).FindString(b.String())
+	if chunk != `<error>
+    <class>*errors.errorString</class>
+    <message>Boom!</message>
+    <backtrace>` {
+		t.Fatal(chunk)
+	}
+
+	// Validate the <request> node.
+	chunk = regexp.MustCompile(`(?s)<request>.*</request>`).FindString(b.String())
+	if chunk != `<request>
+    <url>/query?t=xxx&amp;q=SHOW+x+BY+y+FROM+z&amp;key=sesame&amp;timezone=</url>
+    <component></component>
+    <action></action>
+    <params>
+      <var key="q">SHOW x BY y FROM z</var>
+      <var key="t">xxx</var>
+    </params>
+    <cgi-data>
+      <var key="?q">SHOW x BY y FROM z</var>
+      <var key="?t">xxx</var>
+      <var key="Host">Zulu</var>
+      <var key="Method">GET</var>
+      <var key="Protocol">HTTP/1.1</var>
+    </cgi-data>
+  </request>` {
+		t.Fatal(chunk)
+	}
+}