Commit b91851a4a2d3ea17ca2be0bf5072200fbeeab5cf
1 parent
2d93bb9b
Exists in
master
and in
1 other branch
Implement JSON Api v3
This will allow to capture errors send by airbrake-js.
Showing
4 changed files
with
191 additions
and
1 deletions
Show diff stats
@@ -0,0 +1,34 @@ | @@ -0,0 +1,34 @@ | ||
1 | +class Api::V3::NoticesController < ApplicationController | ||
2 | + skip_before_action :verify_authenticity_token | ||
3 | + skip_before_action :authenticate_user! | ||
4 | + | ||
5 | + respond_to :json | ||
6 | + | ||
7 | + def create | ||
8 | + response.headers['Access-Control-Allow-Origin'] = '*' | ||
9 | + response.headers['Access-Control-Allow-Headers'] = 'origin, content-type, accept' | ||
10 | + | ||
11 | + if !request.options? | ||
12 | + report = JsonParser.new(params).report | ||
13 | + | ||
14 | + if report.valid? | ||
15 | + if report.should_keep? | ||
16 | + report.generate_notice! | ||
17 | + render json: { | ||
18 | + notice: { | ||
19 | + id: report.notice.id | ||
20 | + } | ||
21 | + } | ||
22 | + else | ||
23 | + render text: 'Notice for old app version ignored' | ||
24 | + end | ||
25 | + else | ||
26 | + render text: 'Your API key is unknown', :status => 422 | ||
27 | + end | ||
28 | + else | ||
29 | + render nothing: true | ||
30 | + end | ||
31 | + rescue JsonParser::ParamsError | ||
32 | + render text: 'Invalid request' | ||
33 | + end | ||
34 | +end |
config/routes.rb
@@ -58,9 +58,10 @@ Rails.application.routes.draw do | @@ -58,9 +58,10 @@ Rails.application.routes.draw do | ||
58 | end | 58 | end |
59 | end | 59 | end |
60 | end | 60 | end |
61 | + | ||
62 | + match '/v3/projects/:project_id/notices' => 'v3/notices#create', via: [:post, :options] | ||
61 | end | 63 | end |
62 | 64 | ||
63 | root :to => 'apps#index' | 65 | root :to => 'apps#index' |
64 | - | ||
65 | end | 66 | end |
66 | 67 |
@@ -0,0 +1,86 @@ | @@ -0,0 +1,86 @@ | ||
1 | +class JsonParser | ||
2 | + class ParamsError < StandardError; end | ||
3 | + | ||
4 | + attr_reader :params, :error | ||
5 | + | ||
6 | + def initialize(params) | ||
7 | + @params = params || {} | ||
8 | + end | ||
9 | + | ||
10 | + def report | ||
11 | + attributes = { | ||
12 | + error_class: error['type'], | ||
13 | + message: error['message'], | ||
14 | + backtrace: backtrace, | ||
15 | + request: request, | ||
16 | + server_environment: server_environment, | ||
17 | + api_key: params['key'], | ||
18 | + notifier: params['notifier'], | ||
19 | + user_attributes: user_attributes, | ||
20 | + framework: 'Javascript' | ||
21 | + } | ||
22 | + | ||
23 | + ErrorReport.new(attributes) | ||
24 | + end | ||
25 | + | ||
26 | + private | ||
27 | + | ||
28 | + def error | ||
29 | + raise ParamsError unless params.has_key?('errors') && params['errors'].any? | ||
30 | + @error ||= params['errors'].first | ||
31 | + end | ||
32 | + | ||
33 | + def backtrace | ||
34 | + error['backtrace'].map do |backtrace_line| | ||
35 | + { | ||
36 | + method: backtrace_line['function'], | ||
37 | + file: backtrace_line['file'], | ||
38 | + number: backtrace_line['line'], | ||
39 | + column: backtrace_line['column'] | ||
40 | + } | ||
41 | + end | ||
42 | + end | ||
43 | + | ||
44 | + def server_environment | ||
45 | + { | ||
46 | + 'environment-name' => context['environment'], | ||
47 | + 'hostname' => hostname, | ||
48 | + 'project-root' => context['rootDirectory'], | ||
49 | + 'app-version' => context['version'] | ||
50 | + } | ||
51 | + end | ||
52 | + | ||
53 | + def request | ||
54 | + environment = params['environment'].merge( | ||
55 | + 'HTTP_USER_AGENT' => context['userAgent'] | ||
56 | + ) | ||
57 | + | ||
58 | + { | ||
59 | + 'cgi-data' => environment, | ||
60 | + 'session' => params['session'], | ||
61 | + 'params' => params['params'], | ||
62 | + 'url' => url, | ||
63 | + 'component' => context['component'], | ||
64 | + 'action' => context['action'] | ||
65 | + } | ||
66 | + end | ||
67 | + | ||
68 | + def user_attributes | ||
69 | + hash = context.slice('userId', 'userUsername', 'userName', 'userEmail') | ||
70 | + Hash[hash.map { |key, value| [key.sub(/^user/, ''), value] }] | ||
71 | + end | ||
72 | + | ||
73 | + def url | ||
74 | + context['url'] | ||
75 | + end | ||
76 | + | ||
77 | + def hostname | ||
78 | + URI.parse(url).hostname | ||
79 | + rescue URI::InvalidURIError | ||
80 | + '' | ||
81 | + end | ||
82 | + | ||
83 | + def context | ||
84 | + @context = params['context'] || {} | ||
85 | + end | ||
86 | +end | ||
0 | \ No newline at end of file | 87 | \ No newline at end of file |
@@ -0,0 +1,69 @@ | @@ -0,0 +1,69 @@ | ||
1 | +describe JsonParser do | ||
2 | + let(:app) { Fabricate(:app) } | ||
3 | + | ||
4 | + it 'raises error when errors attribute is missing' do | ||
5 | + expect { | ||
6 | + JsonParser.new({}).report | ||
7 | + }.to raise_error(JsonParser::ParamsError) | ||
8 | + | ||
9 | + expect { | ||
10 | + JsonParser.new({'errors' => []}).report | ||
11 | + }.to raise_error(JsonParser::ParamsError) | ||
12 | + end | ||
13 | + | ||
14 | + it 'parses JSON payload and returns ErrorReport' do | ||
15 | + params = JSON.parse(<<-EOL) | ||
16 | + { | ||
17 | + "notifier":{"name":"airbrake-js-v8","version":"0.3.10","url":"https://github.com/airbrake/airbrake-js"}, | ||
18 | + "errors":[ | ||
19 | + { | ||
20 | + "type":"Error", | ||
21 | + "message":"Error: TestError", | ||
22 | + "backtrace":[ | ||
23 | + {"function":"d","file":"http://localhost:3000/assets/application.js","line":11234,"column":24}, | ||
24 | + {"function":"c","file":"http://localhost:3000/assets/application.js","line":11233,"column":18}, | ||
25 | + {"function":"b","file":"http://localhost:3000/assets/application.js","line":11232,"column":18}, | ||
26 | + {"function":"a","file":"http://localhost:3000/assets/application.js","line":11231,"column":18}, | ||
27 | + {"function":"HTMLDocument.<anonymous>","file":"http://localhost:3000/assets/application.js","line":11236,"column":3}, | ||
28 | + {"function":"fire","file":"http://localhost:3000/assets/application.js","line":1018,"column":34}, | ||
29 | + {"function":"Object.self.fireWith [as resolveWith]","file":"http://localhost:3000/assets/application.js","line":1128,"column":13}, | ||
30 | + {"function":"Function.jQuery.extend.ready","file":"http://localhost:3000/assets/application.js","line":417,"column":15}, | ||
31 | + {"function":"HTMLDocument.DOMContentLoaded","file":"http://localhost:3000/assets/application.js","line":93,"column":14} | ||
32 | + ] | ||
33 | + } | ||
34 | + ], | ||
35 | + "context":{ | ||
36 | + "language":"JavaScript", | ||
37 | + "sourceMapEnabled":true, | ||
38 | + "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36", | ||
39 | + "url":"http://localhost:3000/kontakt", | ||
40 | + "userId":1,"userUsername":"john", | ||
41 | + "userName":"John Doe", | ||
42 | + "userUsername": "john", | ||
43 | + "userEmail":"john.doe@example.org", | ||
44 | + "version":"1.0", | ||
45 | + "component":"ContactsController", | ||
46 | + "action":"show" | ||
47 | + }, | ||
48 | + "params":{"returnTo":"dashboard"}, | ||
49 | + "environment":{"navigator_vendor":"Google Inc."}, | ||
50 | + "session":{"isAdmin":true}, | ||
51 | + "key":"#{app.api_key}" | ||
52 | + } | ||
53 | + EOL | ||
54 | + | ||
55 | + report = JsonParser.new(params).report | ||
56 | + notice = report.generate_notice! | ||
57 | + | ||
58 | + expect(report.error_class).to eq('Error') | ||
59 | + expect(report.message).to eq('Error: TestError') | ||
60 | + expect(report.backtrace.lines.size).to eq(9) | ||
61 | + expect(notice.user_attributes).to include({'Id' => 1, 'Name' => 'John Doe', 'Email' => 'john.doe@example.org', 'Username' => 'john'}) | ||
62 | + expect(notice.session).to include('isAdmin' => true) | ||
63 | + expect(notice.params).to include('returnTo' => 'dashboard') | ||
64 | + expect(notice.env_vars).to include( | ||
65 | + 'navigator_vendor' => 'Google Inc.', | ||
66 | + 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36' | ||
67 | + ) | ||
68 | + end | ||
69 | +end | ||
0 | \ No newline at end of file | 70 | \ No newline at end of file |