├── .gitignore ├── .rakeTasks ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── sentry-api.rb └── sentry-api │ ├── api.rb │ ├── client.rb │ ├── client │ ├── events.rb │ ├── issues.rb │ ├── organizations.rb │ ├── projects.rb │ ├── releases.rb │ └── teams.rb │ ├── configuration.rb │ ├── error.rb │ ├── objectified_hash.rb │ ├── page_links.rb │ ├── paginated_response.rb │ ├── request.rb │ └── version.rb ├── sentry-api.gemspec └── spec ├── fixtures ├── client_keys.json ├── create_client_key.json ├── create_team.json ├── delete_client_key.json ├── delete_project.json ├── issue.json ├── issue_events.json ├── issue_hashes.json ├── latest_event.json ├── oldest_event.json ├── organization.json ├── organization_projects.json ├── organization_stats.json ├── organizations.json ├── project.json ├── project_dsym_files.json ├── project_event.json ├── project_events.json ├── project_issues.json ├── project_stats.json ├── projects.json ├── release.json ├── remove_issue.json ├── team.json ├── update_client_key.json ├── update_issue.json ├── update_organization.json └── update_project.json ├── sentry-api ├── client │ ├── events_spec.rb │ ├── issues_spec.rb │ ├── organizations_spec.rb │ ├── projects_spec.rb │ ├── releases_spec.rb │ └── teams_spec.rb ├── error_spec.rb ├── objectified_hash_spec.rb ├── page_links_spec.rb ├── paginated_response_spec.rb └── request_spec.rb ├── sentry_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /.idea/ 11 | /test/ 12 | *.gem -------------------------------------------------------------------------------- /.rakeTasks: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0 4 | - 2.1 5 | - 2.2 6 | - 2.3 7 | - 2.4 8 | - 2.5 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in sentry.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Thierry Xing 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sentry Ruby API 2 | [![Build Status](https://travis-ci.org/thierryxing/sentry-ruby-api.svg?branch=master)](https://travis-ci.org/thierryxing/sentry-ruby-api) 3 | [![Gem Version](https://badge.fury.io/rb/sentry-api.svg)](https://badge.fury.io/rb/sentry-api) 4 | [![License](https://img.shields.io/badge/license-BSD-red.svg?style=flat)](https://github.com/thierryxing/sentry-ruby-api/blob/master/LICENSE.txt) 5 | 6 | Sentry Ruby API is a Ruby wrapper for the [getsentry/sentry API](https://docs.sentry.io/hosted/api/). 7 | 8 | 9 | ## Installation 10 | Install it from rubygems: 11 | 12 | ```sh 13 | gem install sentry-api 14 | ``` 15 | 16 | Or add to a Gemfile: 17 | 18 | ```ruby 19 | gem 'sentry-api' 20 | ``` 21 | 22 | ## Usage 23 | 24 | Configuration example: 25 | 26 | ```ruby 27 | SentryApi.configure do |config| 28 | config.endpoint = 'http://example.com/api/0' 29 | config.auth_token = 'your_auth_token' 30 | config.default_org_slug = 'sentry-sc' 31 | end 32 | ``` 33 | 34 | (Note: If you are using getsentry.com's hosted service, your endpoint will be `https://sentry.io/api/0/`) 35 | 36 | Usage examples: 37 | 38 | ```ruby 39 | # set an API endpoint 40 | SentryApi.endpoint = 'http://example.com/api/0' 41 | # => "http://example.com/api/0" 42 | 43 | # set a user private token 44 | SentryApi.auth_token = 'your_auth_token' 45 | # => "your_auth_token" 46 | 47 | # configure a proxy server 48 | SentryApi.http_proxy('proxyhost', 8888) 49 | # proxy server w/ basic auth 50 | SentryApi.http_proxy('proxyhost', 8888, 'user', 'pass') 51 | 52 | # list projects 53 | SentryApi.projects 54 | 55 | # initialize a new client 56 | s = SentryApi.client(endpoint: 'https://api.example.com', auth_token: 'your_auth_token', default_org_slug: 'sentry-sc') 57 | 58 | # a paginated response 59 | projects = SentryApi.projects 60 | 61 | # check existence of the next page 62 | projects.has_next_page? 63 | 64 | # retrieve the next page 65 | projects.next_page 66 | 67 | # iterate all projects 68 | projects.auto_paginate do |project| 69 | # do something 70 | end 71 | 72 | # retrieve all projects as an array 73 | projects.auto_paginate 74 | ``` 75 | 76 | ## Development 77 | After checking out the repo, run `bin/setup` to install dependencies. Then, run 78 | `rake spec` to run the tests. You can also run `bin/console` for an interactive 79 | prompt that will allow you to experiment. 80 | 81 | ## License 82 | 83 | Released under the BSD 2-clause license. See LICENSE.txt for details. 84 | 85 | ## Special Thank 86 | Thanks to NARKOZ's [gitlab](https://github.com/NARKOZ/gitlab) ruby wrapper which really gives me a lot of inspiration. 87 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | task :default => :spec 3 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "sentry-api" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/sentry-api.rb: -------------------------------------------------------------------------------- 1 | require 'sentry-api/version' 2 | require 'sentry-api/objectified_hash' 3 | require 'sentry-api/configuration' 4 | require 'sentry-api/error' 5 | require 'sentry-api/page_links' 6 | require 'sentry-api/paginated_response' 7 | require 'sentry-api/request' 8 | require 'sentry-api/api' 9 | require 'sentry-api/client' 10 | 11 | module SentryApi 12 | extend Configuration 13 | extend self 14 | 15 | # Alias for Sentry::Client.new 16 | # 17 | # @return [Sentry::Client] 18 | def self.client(options={}) 19 | options.empty? ? empty_options_client : SentryApi::Client.new(options) 20 | end 21 | 22 | # Delegate to Sentry::Client 23 | def self.method_missing(method, *args, &block) 24 | return super unless client.respond_to?(method) 25 | client.send(method, *args, &block) 26 | end 27 | 28 | # Delegate to Sentry::Client 29 | def respond_to_missing?(method_name, include_private = false) 30 | client.respond_to?(method_name) || super 31 | end 32 | 33 | # Delegate to HTTParty.http_proxy 34 | def self.http_proxy(address=nil, port=nil, username=nil, password=nil) 35 | SentryApi::Request.http_proxy(address, port, username, password) 36 | end 37 | 38 | # Returns an unsorted array of available client methods. 39 | # 40 | # @return [Array] 41 | def self.actions 42 | hidden = /endpoint|auth_token|default_org_slug|get|post|put|delete|validate|set_request_defaults|httparty/ 43 | (SentryApi::Client.instance_methods - Object.methods).reject { |e| e[hidden] } 44 | end 45 | 46 | private 47 | 48 | def empty_options_client 49 | @client ||= SentryApi::Client.new({}) 50 | end 51 | end -------------------------------------------------------------------------------- /lib/sentry-api/api.rb: -------------------------------------------------------------------------------- 1 | module SentryApi 2 | # @private 3 | class API < Request 4 | # @private 5 | attr_accessor(*Configuration::VALID_OPTIONS_KEYS) 6 | 7 | # Creates a new API. 8 | # @raise [Error:MissingCredentials] 9 | def initialize(options={}) 10 | options = SentryApi.options.merge(options) 11 | (Configuration::VALID_OPTIONS_KEYS).each do |key| 12 | send("#{key}=", options[key]) if options[key] 13 | end 14 | set_request_defaults 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/sentry-api/client.rb: -------------------------------------------------------------------------------- 1 | module SentryApi 2 | # Wrapper for the Sentry REST API. 3 | class Client < API 4 | Dir[File.expand_path('../client/*.rb', __FILE__)].each { |f| require f } 5 | 6 | include Organizations 7 | include Projects 8 | include Issues 9 | include Events 10 | include Teams 11 | include Releases 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/sentry-api/client/events.rb: -------------------------------------------------------------------------------- 1 | class SentryApi::Client 2 | 3 | module Events 4 | 5 | # Retrieve an Issue 6 | # 7 | # @example 8 | # SentryApi.issue('120732258') 9 | # 10 | # @param issue_id [String] the ID of the issue to retrieve. 11 | # @return [SentryApi::ObjectifiedHash] 12 | def issue(issue_id) 13 | get("/issues/#{issue_id}/") 14 | end 15 | 16 | # List an Issue’s Events 17 | # 18 | # @example 19 | # SentryApi.issue_events('120732258') 20 | # 21 | # @param issue_id [String] the ID of the issue to retrieve. 22 | # @return [Array] 23 | def issue_events(issue_id) 24 | get("/issues/#{issue_id}/events/") 25 | end 26 | 27 | # List an Issue’s Hashes 28 | # 29 | # @example 30 | # SentryApi.issues_hashes('120732258') 31 | # 32 | # @param issue_id [String] the ID of the issue to retrieve. 33 | # @return [Array] 34 | def issue_hashes(issue_id) 35 | get("/issues/#{issue_id}/hashes/") 36 | end 37 | 38 | # Removes an individual issue. 39 | # 40 | # @example 41 | # SentryApi.remove_issue('120732258') 42 | # 43 | # @param issue_id [String] the ID of the issue to retrieve. 44 | def remove_issue(issue_id) 45 | delete("/issues/#{issue_id}/") 46 | end 47 | 48 | # Update an individual issue. 49 | # 50 | # @example 51 | # SentryApi.update_issue('120732258') 52 | # SentryApi.update_issue('120732258',{status:'resolved'}) 53 | # SentryApi.update_issue('120732258',{status:'resolved', assignedTo:'thierry.xing@gmail.com'}) 54 | # 55 | # @param issue_id [String] the ID of the issue to retrieve. 56 | # @param [Hash] options A customizable set of options. 57 | # @option options [String] :status the new status for the groups. Valid values are "resolved", "unresolved" and "muted". 58 | # @option options [String] :assignedTo the username of the user that should be assigned to this issue. 59 | # @option options [Boolean] :hasSeen in case this API call is invoked with a user context this allows changing of the flag that indicates if the user has seen the event. 60 | # @option options [Boolean] :isBookmarked in case this API call is invoked with a user context this allows changing of the bookmark flag. 61 | # @option options [Boolean] :isSubscribed in case this API call is invoked with a user context this allows changing of the subscribed flag. 62 | # @return 63 | def update_issue(issue_id, options={}) 64 | put("/issues/#{issue_id}/", body: options) 65 | end 66 | 67 | # Retrieves the details of the latest event. 68 | # 69 | # @example 70 | # SentryApi.latest_event('120633628') 71 | # 72 | # @param issue_id [String] the ID of the issue to retrieve. 73 | # @return [SentryApi::ObjectifiedHash] 74 | def latest_event(issue_id) 75 | get("/issues/#{issue_id}/events/latest/") 76 | end 77 | 78 | # Retrieves the details of the oldest event. 79 | # 80 | # @example 81 | # SentryApi.oldest_event('120633628') 82 | # 83 | # @param issue_id [String] the ID of the issue to retrieve. 84 | # @return [SentryApi::ObjectifiedHash] 85 | def oldest_event(issue_id) 86 | get("/issues/#{issue_id}/events/oldest/") 87 | end 88 | 89 | end 90 | 91 | end 92 | -------------------------------------------------------------------------------- /lib/sentry-api/client/issues.rb: -------------------------------------------------------------------------------- 1 | class SentryApi::Client 2 | module Issues 3 | # List Issues 4 | # 5 | # @example 6 | # SentryApi.project_issues('project-slug', {'query': 'is:unresolved Build-version:6.5.0'}) 7 | # 8 | # @param project_slug [String] the slug of the project the client keys belong to. 9 | # @param [Hash] options A customizable set of options. @option options [String] :statsPeriod an optional stat period (can be one of "24h", "14d", and ""). 10 | # @option options [String] :query an optional Sentry structured search query. If not provided an implied "is:resolved" is assumed.) 11 | # @return [Array] 12 | def issues(project_slug, options={}) 13 | get("/projects/#{@default_org_slug}/#{project_slug}/issues/", query: options) 14 | end 15 | 16 | # Batch update issues 17 | # 18 | # @example 19 | # SentryApi.update_client_key('project-slug', ['123', '456'], status:'ignored') 20 | # 21 | # @param project_slug [String] the slug of the project the client keys belong to. 22 | # @param issue_ids [Array] An array of issue ids which are to be updated. 23 | # @option options [Object] An object containing the issue fields which are to be updated. 24 | # @return [Array] 25 | def batch_update_issues(project_slug, issue_ids=[], options={}) 26 | put("/projects/#{@default_org_slug}/#{project_slug}/issues/?id=#{issue_ids.join('&id=')}", body: options) 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/sentry-api/client/organizations.rb: -------------------------------------------------------------------------------- 1 | class SentryApi::Client 2 | 3 | module Organizations 4 | # List your Organizations. 5 | # 6 | # @example 7 | # SentryApi.organizations 8 | # 9 | # @param member [Boolean] Restrict results to organizations which you have membership 10 | # @return [Array] 11 | def organizations(member=false) 12 | get("/organizations/", query: {member: member}) 13 | end 14 | 15 | # List an Organization’s Projects 16 | # 17 | # @example 18 | # SentryApi.organization_projects 19 | # SentryApi.organization_projects('slug') 20 | # 21 | # @return [Array] 22 | def organization_projects 23 | get("/organizations/#{@default_org_slug}/projects/") 24 | end 25 | 26 | # Retrieve an Organization 27 | # 28 | # @example 29 | # SentryApi.organization 30 | # SentryApi.organization('slug') 31 | # 32 | # @return [SentryApi::ObjectifiedHash] 33 | def organization 34 | get("/organizations/#{@default_org_slug}/") 35 | end 36 | 37 | # Update an Organization 38 | # 39 | # @example 40 | # SentryApi.update_organization('slug') 41 | # SentryApi.update_organization('slug',{name:'new-name'}) 42 | # SentryApi.update_organization('slug',{name:'new-name', slug:'new-slug'}) 43 | # 44 | # @param [Hash] options A customizable set of options. 45 | # @option options [String] :name an optional new name for the organization. 46 | # @option options [String] :slug an optional new slug for the organization. Needs to be available and unique. 47 | # @return [SentryApi::ObjectifiedHash] 48 | def update_organization(options={}) 49 | put("/organizations/#{@default_org_slug}/", body: options) 50 | end 51 | 52 | # Retrieve Event Counts for an Organization 53 | # 54 | # @example 55 | # SentryApi.organization_stats('slug') 56 | # SentryApi.organization_stats('slug', {stat:'received', since:'1472158800'}) 57 | # 58 | # @param [Hash] options A customizable set of options. 59 | # @option options [String] :stat the name of the stat to query ("received", "rejected", "blacklisted") 60 | # @option options [Timestamp] :since a timestamp to set the start of the query in seconds since UNIX epoch. 61 | # @option options [Timestamp] :until a timestamp to set the end of the query in seconds since UNIX epoch. 62 | # @option options [String] :resolution an explicit resolution to search for (eg: 10s). This should not be used unless you are familiar with Sentry’s internals as it’s restricted to pre-defined values. 63 | # @return [Array] 64 | def organization_stats(options={}) 65 | get("/organizations/#{@default_org_slug}/stats/", query: options) 66 | end 67 | 68 | # Create a new team bound to an organization 69 | # 70 | # @example 71 | # SentryApi.create_project('team-slug', {name:'team-name', slug:'team-slug'}) 72 | # 73 | # @param [Hash] options A customizable set of options. 74 | # @option options [String] :name the name for the new team. 75 | # @option options [String] :slug optionally a slug for the new team. If it’s not provided a slug is generated from the name. 76 | # @return [SentryApi::ObjectifiedHash] 77 | def create_team(options={}) 78 | post("/organizations/#{@default_org_slug}/teams/", body: options) 79 | end 80 | 81 | # Return a list of teams bound to a organization. 82 | # 83 | # @example 84 | # SentryApi.organization_teams('team-slug') 85 | # 86 | # @return [Array] 87 | def organization_teams 88 | get("/organizations/#{@default_org_slug}/teams/") 89 | end 90 | 91 | end 92 | 93 | end -------------------------------------------------------------------------------- /lib/sentry-api/client/projects.rb: -------------------------------------------------------------------------------- 1 | class SentryApi::Client 2 | 3 | module Projects 4 | # List your Projects 5 | # 6 | # @example 7 | # SentryApi.projects 8 | # 9 | # @return [Array] 10 | def projects 11 | get("/projects/") 12 | end 13 | 14 | # Retrieve a Project 15 | # 16 | # @example 17 | # SentryApi.project('project-slug') 18 | # 19 | # @param project_slug [String] the slug of the project to retrieve. 20 | # @return [SentryApi::ObjectifiedHash] 21 | def project(project_slug) 22 | get("/projects/#{@default_org_slug}/#{project_slug}/") 23 | end 24 | 25 | # Update a Project 26 | # 27 | # @example 28 | # SentryApi.update_project('project-slug', {name:'new-name', slug:'new-slug', is_bookmarked:false}) 29 | # 30 | # @param project_slug [String] the slug of the project to retrieve. 31 | # @param [Hash] options A customizable set of options. 32 | # @option options [String] :name the new name for the project. 33 | # @option options [String] :slug the new slug for the project. 34 | # @option options [String] :isBookmarked in case this API call is invoked with a user context this allows changing of the bookmark flag. 35 | # @option options [Hash] optional options to override in the project settings. 36 | # @return [SentryApi::ObjectifiedHash] 37 | def update_project(project_slug, options={}) 38 | put("/projects/#{@default_org_slug}/#{project_slug}/", body: options) 39 | end 40 | 41 | # Delete a Project. 42 | # 43 | # @example 44 | # SentryApi.delete_project('project-slug') 45 | # 46 | # @param project_slug [String] the slug of the project to delete. 47 | def delete_project(project_slug) 48 | delete("/projects/#{@default_org_slug}/#{project_slug}/") 49 | end 50 | 51 | # Retrieve Event Counts for an Project 52 | # 53 | # @example 54 | # SentryApi.project_stats('slug') 55 | # SentryApi.project_stats('slug', {stat:'received', since:'1472158800'}) 56 | # 57 | # @param project_slug [String] the slug of the project. 58 | # @param [Hash] options A customizable set of options. 59 | # @option options [String] :stat the name of the stat to query ("received", "rejected", "blacklisted") 60 | # @option options [Timestamp] :since a timestamp to set the start of the query in seconds since UNIX epoch. 61 | # @option options [Timestamp] :until a timestamp to set the end of the query in seconds since UNIX epoch. 62 | # @option options [String] :resolution an explicit resolution to search for (eg: 10s). This should not be used unless you are familiar with Sentry’s internals as it’s restricted to pre-defined values. 63 | # @return [Array] 64 | def project_stats(project_slug, options={}) 65 | get("/projects/#{@default_org_slug}/#{project_slug}/stats/", query: options) 66 | end 67 | 68 | # Upload a new dsym file for the given release 69 | # 70 | # @example 71 | # SentryApi.upload_dsym_files('project-slug','/path/to/file') 72 | # 73 | # @param project_slug [String] the slug of the project to list the dsym files of. 74 | # @param file_path [String] the absolute file path of the dsym file. 75 | # @param organization_slug [String] the slug of the organization. 76 | # @return [Array] 77 | def upload_dsym_files(project_slug, file_path) 78 | upload("/projects/#{@default_org_slug}/#{project_slug}/files/dsyms/", body: {file: File.new(file_path)}) 79 | end 80 | 81 | # List a Project’s DSym Files. 82 | # 83 | # @example 84 | # SentryApi.project_dsym_files('project-slug') 85 | # 86 | # @param project_slug [String] the slug of the project to list the dsym files of. 87 | # @return [Array] 88 | def project_dsym_files(project_slug) 89 | get("/projects/#{@default_org_slug}/#{project_slug}/files/dsyms/") 90 | end 91 | 92 | # List a Project’s Client Keys. 93 | # 94 | # @example 95 | # SentryApi.client_keys('project-slug') 96 | # 97 | # @param project_slug [String] the slug of the project the client keys belong to. 98 | # @return [Array] 99 | def client_keys(project_slug) 100 | get("/projects/#{@default_org_slug}/#{project_slug}/keys/") 101 | end 102 | 103 | # Create a new Client Key. 104 | # 105 | # @example 106 | # SentryApi.create_client_key('project-slug','new-name') 107 | # 108 | # @param project_slug [String] the slug of the project the client keys belong to. 109 | # @param [Hash] options A customizable set of options. 110 | # @option options [String] :name the name for the new key. 111 | # @return [SentryApi::ObjectifiedHash] 112 | def create_client_key(project_slug, options={}) 113 | post("/projects/#{@default_org_slug}/#{project_slug}/keys/", body: options) 114 | end 115 | 116 | # Delete a Client Key. 117 | # 118 | # @example 119 | # SentryApi.delete_client_key('project-slug','87c990582e07446b9907b357fc27730e') 120 | # 121 | # @param project_slug [String] the slug of the project the client keys belong to. 122 | # @param key_id [String] the ID of the key to delete. 123 | def delete_client_key(project_slug, key_id) 124 | delete("/projects/#{@default_org_slug}/#{project_slug}/keys/#{key_id}/") 125 | end 126 | 127 | # Update a Client Key 128 | # 129 | # @example 130 | # SentryApi.update_client_key('project-slug','87c990582e07446b9907b357fc27730e',{name:'new-name'}) 131 | # 132 | # @param project_slug [String] the slug of the project the client keys belong to. 133 | # @param key_id [String] the ID of the key to update. 134 | # @param [Hash] options A customizable set of options. 135 | # @option options [String] :name the new name for the client key. 136 | # @return [Array] 137 | def update_client_key(project_slug, key_id, options={}) 138 | put("/projects/#{@default_org_slug}/#{project_slug}/keys/#{key_id}/", body: options) 139 | end 140 | 141 | # Return a list of sampled events bound to a project. 142 | # 143 | # @example 144 | # SentryApi.project_events('project-slug') 145 | # 146 | # @param project_slug [String] the slug of the project the client keys belong to. 147 | # @return [Array] 148 | def project_events(project_slug) 149 | get("/projects/#{@default_org_slug}/#{project_slug}/events/") 150 | end 151 | 152 | # Return a list of issues (groups) bound to a project. All parameters are supplied as query string parameters. 153 | # 154 | # @example 155 | # SentryApi.project_event('project-slug', 'event-id') 156 | # 157 | # @param project_slug [String] the slug of the project the client keys belong to. 158 | # @param event_id [String] the hexadecimal ID of the event to retrieve (as reported by the raven client) 159 | # @return [SentryApi::ObjectifiedHash] 160 | def project_event(project_slug, event_id) 161 | get("/projects/#{@default_org_slug}/#{project_slug}/events/#{event_id}/") 162 | end 163 | 164 | # List a Project’s Issues 165 | # 166 | # @example 167 | # SentryApi.project_issues('project-slug', {'query': 'is:unresolved Build-version:6.5.0'}) 168 | # 169 | # @param project_slug [String] the slug of the project the client keys belong to. 170 | # @param [Hash] options A customizable set of options. 171 | # @option options [String] :statsPeriod an optional stat period (can be one of "24h", "14d", and ""). 172 | # @option options [String] :query an optional Sentry structured search query. If not provided an implied "is:resolved" is assumed.) 173 | # @return [Array] 174 | def project_issues(project_slug, options={}) 175 | get("/projects/#{@default_org_slug}/#{project_slug}/issues/", query: options) 176 | end 177 | 178 | end 179 | 180 | end 181 | -------------------------------------------------------------------------------- /lib/sentry-api/client/releases.rb: -------------------------------------------------------------------------------- 1 | class SentryApi::Client 2 | 3 | module Releases 4 | 5 | # Create a new release for the given project. 6 | # Releases are used by Sentry to improve it’s error reporting abilities by correlating first seen events with the release that might have introduced the problem. 7 | # 8 | # @example 9 | # SentryApi.create_release('project-slug',{version:'1.0', ref:'6ba09a7c53235ee8a8fa5ee4c1ca8ca886e7fdbb'}) 10 | # 11 | # @param project_slug [String] the slug of the project the client keys belong to. 12 | # @param [Hash] options A customizable set of options. 13 | # @option options [String] :version a version identifier for this release. Can be a version number, a commit hash etc. 14 | # @option options [String] :ref an optional commit reference. This is useful if a tagged version has been provided. 15 | # @option options [String] :url a URL that points to the release. This can be the path to an online interface to the sourcecode for instance. 16 | # @option options [Timestamp] :dateStarted an optional date that indicates when the release process started. 17 | # @option options [Timestamp] :dateReleased an optional date that indicates when the release went live. If not provided the current time is assumed. 18 | # @return 19 | def create_release(project_slug, options={}) 20 | post("/projects/#{@default_org_slug}/#{project_slug}/releases/", body: options) 21 | end 22 | 23 | # Permanently remove a release and all of its files. 24 | # 25 | # @example 26 | # SentryApi.delete_release('project-slug','1.0') 27 | # 28 | # @param project_slug [String] the slug of the project to delete the release of. 29 | # @param version [String] the version identifier of the release. 30 | def delete_release(project_slug, version) 31 | delete("/projects/#{@default_org_slug}/#{project_slug}/releases/#{version}/") 32 | end 33 | 34 | # List a Project’s Releases 35 | # 36 | # @example 37 | # SentryApi.releases('project-slug') 38 | # 39 | # @param project_slug [String] the slug of the project to list the releases of. 40 | # @return [Array] 41 | def releases(project_slug) 42 | get("/projects/#{@default_org_slug}/#{project_slug}/releases/") 43 | end 44 | 45 | # Retrieve a Release 46 | # 47 | # @example 48 | # SentryApi.release('project-slug','1.0') 49 | # 50 | # @param project_slug [String] the slug of the project to retrieve the release of. 51 | # @param version [String] the version identifier of the release. 52 | # @return 53 | def release(project_slug, version) 54 | get("/projects/#{@default_org_slug}/#{project_slug}/releases/#{version}/") 55 | end 56 | 57 | # Update a Release 58 | # 59 | # @example 60 | # SentryApi.update('project-slug', {ref:'6ba09a7c53235ee8a8fa5ee4c1ca8ca886e7fdbb'}) 61 | # 62 | # @param project_slug [String] the slug of the project to retrieve the release of. 63 | # @param version [String] the version identifier of the release. 64 | # @option options [String] :ref an optional commit reference. This is useful if a tagged version has been provided. 65 | # @option options [String] :url a URL that points to the release. This can be the path to an online interface to the sourcecode for instance. 66 | # @option options [Timestamp] :dateStarted an optional date that indicates when the release process started. 67 | # @option options [Timestamp] :dateReleased an optional date that indicates when the release went live. If not provided the current time is assumed. 68 | # @return 69 | def update_release(project_slug, version, options={}) 70 | put("/projects/#{@default_org_slug}/#{project_slug}/releases/#{version}/", body: options) 71 | end 72 | 73 | end 74 | 75 | end -------------------------------------------------------------------------------- /lib/sentry-api/client/teams.rb: -------------------------------------------------------------------------------- 1 | class SentryApi::Client 2 | 3 | module Teams 4 | 5 | # Create a new project bound to a team. 6 | # 7 | # @example 8 | # SentryApi.create_project('team-slug', {name:'team-name'}) 9 | # 10 | # @param team_slug [String] the slug of the team 11 | # @param [Hash] options A customizable set of options. 12 | # @option options [String] :name the name for the new project. 13 | # @option options [String] :slug optionally a slug for the new project. If it’s not provided a slug is generated from the name. 14 | # @return [SentryApi::ObjectifiedHash] 15 | def create_project(team_slug, options={}) 16 | post("/teams/#{@default_org_slug}/#{team_slug}/projects/", body: options) 17 | end 18 | 19 | # Schedules a team for deletion 20 | # 21 | # @example 22 | # SentryApi.delete_team('team-slug') 23 | # 24 | # @param team_slug [String] the slug of the team 25 | def delete_team(team_slug) 26 | delete("/teams/#{@default_org_slug}/#{team_slug}/") 27 | end 28 | 29 | # Return a list of projects bound to a team 30 | # 31 | # @example 32 | # SentryApi.delete_team('team-slug') 33 | # 34 | # @param team_slug [String] the slug of the team 35 | # @return [Array] 36 | def team_projects(team_slug) 37 | get("/teams/#{@default_org_slug}/#{team_slug}/projects/") 38 | end 39 | 40 | # Return details on an individual team. 41 | # 42 | # @example 43 | # SentryApi.team_projects('team-slug') 44 | # 45 | # @param team_slug [String] the slug of the team 46 | # @return [SentryApi::ObjectifiedHash] 47 | def team(team_slug) 48 | get("/teams/#{@default_org_slug}/#{team_slug}/") 49 | end 50 | 51 | # Update various attributes and configurable settings for the given team. 52 | # 53 | # @example 54 | # SentryApi.update_team('team-slug', {name:'team-name'}) 55 | # 56 | # @param team_slug [String] the slug of the team 57 | # @param [Hash] options A customizable set of options. 58 | # @option options [String] :name the name for the new project. 59 | # @option options [String] :slug optionally a slug for the new project. If it’s not provided a slug is generated from the name. 60 | # @return [SentryApi::ObjectifiedHash] 61 | def update_team(team_slug, options={}) 62 | get("/teams/#{@default_org_slug}/#{team_slug}/", body: options) 63 | end 64 | 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/sentry-api/configuration.rb: -------------------------------------------------------------------------------- 1 | module SentryApi 2 | # Defines constants and methods related to configuration. 3 | module Configuration 4 | # An array of valid keys in the options hash when configuring a Sentry::API. 5 | VALID_OPTIONS_KEYS = [:endpoint, :auth_token, :default_org_slug, :httparty].freeze 6 | 7 | # The user agent that will be sent to the API endpoint if none is set. 8 | DEFAULT_USER_AGENT = "Sentry Ruby Gem #{SentryApi::VERSION}".freeze 9 | 10 | # @private 11 | attr_accessor(*VALID_OPTIONS_KEYS) 12 | 13 | # Sets all configuration options to their default values 14 | # when this module is extended. 15 | def self.extended(base) 16 | base.reset 17 | end 18 | 19 | # Convenience method to allow configuration options to be set in a block. 20 | def configure 21 | yield self 22 | end 23 | 24 | # Creates a hash of options and their values. 25 | def options 26 | VALID_OPTIONS_KEYS.inject({}) do |option, key| 27 | option.merge!(key => send(key)) 28 | end 29 | end 30 | 31 | # Resets all configuration options to the defaults. 32 | def reset 33 | self.endpoint = ENV['SENTRY_API_ENDPOINT'] 34 | self.auth_token = ENV['SENTRY_API_AUTH_TOKEN'] 35 | self.default_org_slug = ENV['SENTRY_API_DEFAULT_ORG_SLUG'] 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/sentry-api/error.rb: -------------------------------------------------------------------------------- 1 | module SentryApi 2 | module Error 3 | # Custom error class for rescuing from all Sentry errors. 4 | class Error < StandardError; 5 | end 6 | 7 | # Raised when API endpoint credentials not configured. 8 | class MissingCredentials < Error; 9 | end 10 | 11 | # Raised when impossible to parse response body. 12 | class Parsing < Error; 13 | end 14 | 15 | # Custom error class for rescuing from HTTP response errors. 16 | class ResponseError < Error 17 | def initialize(response) 18 | @response = response 19 | super(build_error_message) 20 | end 21 | 22 | # Status code returned in the http response. 23 | # 24 | # @return [Integer] 25 | def response_status 26 | @response.code 27 | end 28 | 29 | private 30 | 31 | # Human friendly message. 32 | # 33 | # @return [String] 34 | def build_error_message 35 | parsed_response = @response.parsed_response 36 | message = parsed_response.message || parsed_response.error 37 | 38 | "Server responded with code #{@response.code}, message: " \ 39 | "#{handle_message(message)}. " \ 40 | "Request URI: #{@response.request.base_uri}#{@response.request.path}" 41 | end 42 | 43 | # Handle error response message in case of nested hashes 44 | def handle_message(message) 45 | case message 46 | when SentryApi::ObjectifiedHash 47 | message.to_h.sort.map do |key, val| 48 | "'#{key}' #{(val.is_a?(Hash) ? val.sort.map { |k, v| "(#{k}: #{v.join(' ')})" } : val).join(' ')}" 49 | end.join(', ') 50 | when Array 51 | message.join(' ') 52 | else 53 | message 54 | end 55 | end 56 | end 57 | 58 | # Raised when API endpoint returns the HTTP status code 400. 59 | class BadRequest < ResponseError; 60 | end 61 | 62 | # Raised when API endpoint returns the HTTP status code 401. 63 | class Unauthorized < ResponseError; 64 | end 65 | 66 | # Raised when API endpoint returns the HTTP status code 403. 67 | class Forbidden < ResponseError; 68 | end 69 | 70 | # Raised when API endpoint returns the HTTP status code 404. 71 | class NotFound < ResponseError; 72 | end 73 | 74 | # Raised when API endpoint returns the HTTP status code 405. 75 | class MethodNotAllowed < ResponseError; 76 | end 77 | 78 | # Raised when API endpoint returns the HTTP status code 409. 79 | class Conflict < ResponseError; 80 | end 81 | 82 | # Raised when API endpoint returns the HTTP status code 422. 83 | class Unprocessable < ResponseError; 84 | end 85 | 86 | # Raised when API endpoint returns the HTTP status code 500. 87 | class InternalServerError < ResponseError; 88 | end 89 | 90 | # Raised when API endpoint returns the HTTP status code 502. 91 | class BadGateway < ResponseError; 92 | end 93 | 94 | # Raised when API endpoint returns the HTTP status code 503. 95 | class ServiceUnavailable < ResponseError; 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/sentry-api/objectified_hash.rb: -------------------------------------------------------------------------------- 1 | module SentryApi 2 | # Converts hashes to the objects. 3 | class ObjectifiedHash 4 | # Creates a new ObjectifiedHash object. 5 | def initialize(hash) 6 | @hash = hash 7 | @data = hash.inject({}) do |data, (key, value)| 8 | value = ObjectifiedHash.new(value) if value.is_a? Hash 9 | data[key.to_s] = value 10 | data 11 | end 12 | end 13 | 14 | # @return [Hash] The original hash. 15 | def to_hash 16 | @hash 17 | end 18 | 19 | alias_method :to_h, :to_hash 20 | 21 | # @return [String] Formatted string with the class name, object id and original hash. 22 | def inspect 23 | "#<#{self.class}:#{object_id} {hash: #{@hash.inspect}}" 24 | end 25 | 26 | # Delegate to ObjectifiedHash. 27 | def method_missing(key) 28 | @data.key?(key.to_s) ? @data[key.to_s] : nil 29 | end 30 | 31 | def respond_to_missing?(method_name, include_private = false) 32 | @hash.keys.map(&:to_sym).include?(method_name.to_sym) || super 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/sentry-api/page_links.rb: -------------------------------------------------------------------------------- 1 | module SentryApi 2 | # Parses link header. 3 | # 4 | # @private 5 | class PageLinks 6 | HEADER_LINK = 'Link'.freeze 7 | DELIM_LINKS = ','.freeze 8 | LINK_REGEX = /<([^>]+)>; rel=\"([^\"]+)\"; results=\"([^\"]+)\"/ 9 | METAS = %w(previous next) 10 | 11 | attr_accessor(*METAS) 12 | 13 | def initialize(headers) 14 | link_header = headers[HEADER_LINK] 15 | 16 | if link_header && link_header =~ /(previous|next)/ 17 | extract_links(link_header) 18 | end 19 | end 20 | 21 | private 22 | 23 | def extract_links(header) 24 | header.split(DELIM_LINKS).each do |link| 25 | LINK_REGEX.match(link.strip) do |match| 26 | url, meta, results = match[1], match[2], match[3] 27 | next if !url or !meta or !(results == "true") or METAS.index(meta).nil? 28 | self.send("#{meta}=", url) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/sentry-api/paginated_response.rb: -------------------------------------------------------------------------------- 1 | module SentryApi 2 | # Wrapper class of paginated response. 3 | class PaginatedResponse 4 | attr_accessor :client 5 | 6 | def initialize(array) 7 | @array = array 8 | end 9 | 10 | def ==(other) 11 | @array == other 12 | end 13 | 14 | def inspect 15 | @array.inspect 16 | end 17 | 18 | def method_missing(name, *args, &block) 19 | if @array.respond_to?(name) 20 | @array.send(name, *args, &block) 21 | else 22 | super 23 | end 24 | end 25 | 26 | def respond_to_missing?(method_name, include_private = false) 27 | super || @array.respond_to?(method_name, include_private) 28 | end 29 | 30 | def parse_headers!(headers) 31 | @links = PageLinks.new headers 32 | end 33 | 34 | def each_page 35 | current = self 36 | yield current 37 | while current.has_next_page? 38 | current = current.next_page 39 | yield current 40 | end 41 | end 42 | 43 | def auto_paginate 44 | response = block_given? ? nil : [] 45 | each_page do |page| 46 | if block_given? 47 | page.each do |item| 48 | yield item 49 | end 50 | else 51 | response += page 52 | end 53 | end 54 | response 55 | end 56 | 57 | def has_next_page? 58 | !(@links.nil? || @links.next.nil?) 59 | end 60 | 61 | def next_page 62 | return nil if @client.nil? || !has_next_page? 63 | path = @links.next.sub(/#{@client.endpoint}/, '') 64 | @client.get(path) 65 | end 66 | 67 | def has_prev_page? 68 | !(@links.nil? || @links.previous.nil?) 69 | end 70 | 71 | def prev_page 72 | return nil if @client.nil? || !has_prev_page? 73 | path = @links.previous.sub(/#{@client.endpoint}/, '') 74 | @client.get(path) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/sentry-api/request.rb: -------------------------------------------------------------------------------- 1 | require 'httmultiparty' 2 | require 'json' 3 | 4 | module SentryApi 5 | # @private 6 | class Request 7 | include HTTMultiParty 8 | 9 | format :json 10 | headers "Content-Type" => "application/json" 11 | parser proc { |body, _| parse(body) } 12 | attr_accessor :auth_token, :endpoint, :default_org_slug 13 | 14 | # Converts the response body to an ObjectifiedHash. 15 | def self.parse(body) 16 | body = decode(body) 17 | 18 | if body.is_a? Hash 19 | ObjectifiedHash.new body 20 | elsif body.is_a? Array 21 | if body[0].is_a? Array 22 | body 23 | else 24 | PaginatedResponse.new(body.collect! { |e| ObjectifiedHash.new(e) }) 25 | end 26 | elsif body 27 | true 28 | elsif !body 29 | false 30 | elsif body.nil? 31 | false 32 | else 33 | raise Error::Parsing.new "Couldn't parse a response body" 34 | end 35 | end 36 | 37 | # Decodes a JSON response into Ruby object. 38 | def self.decode(response) 39 | JSON.load response 40 | rescue JSON::ParserError 41 | raise Error::Parsing.new "The response is not a valid JSON" 42 | end 43 | 44 | def get(path, options={}) 45 | set_httparty_config(options) 46 | set_authorization_header(options) 47 | validate self.class.get(@endpoint + path, options) 48 | end 49 | 50 | def post(path, options={}) 51 | set_httparty_config(options) 52 | set_json_body(options) 53 | set_authorization_header(options, path) 54 | validate self.class.post(@endpoint + path, options) 55 | end 56 | 57 | def put(path, options={}) 58 | set_httparty_config(options) 59 | set_json_body(options) 60 | set_authorization_header(options) 61 | validate self.class.put(@endpoint + path, options) 62 | end 63 | 64 | def delete(path, options={}) 65 | set_httparty_config(options) 66 | set_authorization_header(options) 67 | validate self.class.delete(@endpoint + path, options) 68 | end 69 | 70 | def upload(path, options={}) 71 | set_httparty_config(options) 72 | set_authorization_header(options) 73 | validate self.class.post(@endpoint + path, options) 74 | end 75 | 76 | # Checks the response code for common errors. 77 | # Returns parsed response for successful requests. 78 | def validate(response) 79 | error_klass = case response.code 80 | when 400 then 81 | Error::BadRequest 82 | when 401 then 83 | Error::Unauthorized 84 | when 403 then 85 | Error::Forbidden 86 | when 404 then 87 | Error::NotFound 88 | when 405 then 89 | Error::MethodNotAllowed 90 | when 409 then 91 | Error::Conflict 92 | when 422 then 93 | Error::Unprocessable 94 | when 500 then 95 | Error::InternalServerError 96 | when 502 then 97 | Error::BadGateway 98 | when 503 then 99 | Error::ServiceUnavailable 100 | end 101 | 102 | fail error_klass.new(response) if error_klass 103 | 104 | parsed = response.parsed_response 105 | parsed.client = self if parsed.respond_to?(:client=) 106 | parsed.parse_headers!(response.headers) if parsed.respond_to?(:parse_headers!) 107 | parsed 108 | end 109 | 110 | # Sets a base_uri and default_params for requests. 111 | # @raise [Error::MissingCredentials] if endpoint not set. 112 | def set_request_defaults 113 | self.class.default_params 114 | raise Error::MissingCredentials.new("Please set an endpoint to API") unless @endpoint 115 | end 116 | 117 | private 118 | 119 | # Sets a Authorization header for requests. 120 | # @raise [Error::MissingCredentials] if auth_token and auth_token are not set. 121 | def set_authorization_header(options, path=nil) 122 | unless path == '/session' 123 | raise Error::MissingCredentials.new("Please provide a auth_token for user") unless @auth_token 124 | options[:headers] = {'Authorization' => "Bearer #{@auth_token}"} 125 | end 126 | end 127 | 128 | # Set http post or put body as json string if content type is application/json 129 | def set_json_body(options) 130 | headers = self.class.headers 131 | if headers and headers["Content-Type"] == "application/json" 132 | options[:body] = options[:body].to_json 133 | end 134 | end 135 | 136 | # Set HTTParty configuration 137 | # @see https://github.com/jnunemaker/httparty 138 | def set_httparty_config(options) 139 | options.merge!(httparty) if httparty 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/sentry-api/version.rb: -------------------------------------------------------------------------------- 1 | module SentryApi 2 | VERSION = "0.3.4" 3 | end 4 | -------------------------------------------------------------------------------- /sentry-api.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'sentry-api/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "sentry-api" 8 | spec.version = SentryApi::VERSION 9 | spec.authors = ["Thierry Xing"] 10 | spec.email = ["thierry.xing@gmail.com"] 11 | spec.licenses = ['BSD'] 12 | spec.summary = %q{Ruby client for Sentry API} 13 | spec.description = %q{A Ruby wrapper for the Sentry API} 14 | spec.homepage = "https://github.com/thierryxing/sentry-ruby-api" 15 | 16 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 17 | # to allow pushing to a single host or delete this section to allow pushing to any host. 18 | if spec.respond_to?(:metadata) 19 | spec.metadata['allowed_push_host'] = "https://rubygems.org" 20 | else 21 | raise "RubyGems 2.0 or newer is required to protect against public gem pushes." 22 | end 23 | 24 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 25 | spec.bindir = "exe" 26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 27 | spec.require_paths = ["lib"] 28 | 29 | spec.add_runtime_dependency 'httmultiparty', "~> 0.3.16" 30 | spec.add_development_dependency "bundler", "~> 1.12" 31 | spec.add_development_dependency "rake", "~> 10.0" 32 | spec.add_development_dependency 'rspec', "~> 3.5.0", '>= 3.5.0' 33 | spec.add_development_dependency 'webmock', "~> 2.1.0", '>= 2.1.0' 34 | spec.add_development_dependency 'yard', "~> 0.9.5" 35 | end 36 | -------------------------------------------------------------------------------- /spec/fixtures/client_keys.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "Fabulous Key", 4 | "dsn": { 5 | "secret": "https://95729535ea81422dba6c951e714a0174:a6b380407fc5443fb30d239670f8dee8@app.getsentry.com/2", 6 | "csp": "https://app.getsentry.com/api/2/csp-report/?sentry_key=95729535ea81422dba6c951e714a0174", 7 | "public": "https://95729535ea81422dba6c951e714a0174@app.getsentry.com/2" 8 | }, 9 | "secret": "a6b380407fc5443fb30d239670f8dee8", 10 | "id": "95729535ea81422dba6c951e714a0174", 11 | "dateCreated": "2016-08-26T20:01:03.047Z", 12 | "public": "95729535ea81422dba6c951e714a0174" 13 | } 14 | ] -------------------------------------------------------------------------------- /spec/fixtures/create_client_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Fabulous Key", 3 | "dsn": { 4 | "secret": "https://95729535ea81422dba6c951e714a0174:a6b380407fc5443fb30d239670f8dee8@app.getsentry.com/2", 5 | "csp": "https://app.getsentry.com/api/2/csp-report/?sentry_key=95729535ea81422dba6c951e714a0174", 6 | "public": "https://95729535ea81422dba6c951e714a0174@app.getsentry.com/2" 7 | }, 8 | "secret": "a6b380407fc5443fb30d239670f8dee8", 9 | "id": "95729535ea81422dba6c951e714a0174", 10 | "dateCreated": "2016-08-26T20:01:03.047Z", 11 | "public": "95729535ea81422dba6c951e714a0174" 12 | } -------------------------------------------------------------------------------- /spec/fixtures/create_team.json: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "ancient-gabelers", 3 | "name": "Ancient Gabelers", 4 | "hasAccess": true, 5 | "isPending": false, 6 | "dateCreated": "2016-08-30T21:23:02.065Z", 7 | "isMember": false, 8 | "id": "3" 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixtures/delete_client_key.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /spec/fixtures/delete_project.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /spec/fixtures/issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "unresolved", 3 | "lastSeen": "2016-08-30T01:11:03Z", 4 | "userReportCount": 0, 5 | "id": "1", 6 | "userCount": 0, 7 | "stats": { 8 | "30d": [ 9 | [ 10 | 1469923200, 11 | 12102 12 | ], 13 | [ 14 | 1470009600, 15 | 15787 16 | ], 17 | [ 18 | 1470096000, 19 | 13029 20 | ], 21 | [ 22 | 1470182400, 23 | 13118 24 | ], 25 | [ 26 | 1470268800, 27 | 12554 28 | ], 29 | [ 30 | 1470355200, 31 | 11602 32 | ], 33 | [ 34 | 1470441600, 35 | 14314 36 | ], 37 | [ 38 | 1470528000, 39 | 12500 40 | ], 41 | [ 42 | 1470614400, 43 | 12521 44 | ], 45 | [ 46 | 1470700800, 47 | 11587 48 | ], 49 | [ 50 | 1470787200, 51 | 12572 52 | ], 53 | [ 54 | 1470873600, 55 | 14235 56 | ], 57 | [ 58 | 1470960000, 59 | 14530 60 | ], 61 | [ 62 | 1471046400, 63 | 14487 64 | ], 65 | [ 66 | 1471132800, 67 | 12891 68 | ], 69 | [ 70 | 1471219200, 71 | 11672 72 | ], 73 | [ 74 | 1471305600, 75 | 14050 76 | ], 77 | [ 78 | 1471392000, 79 | 14193 80 | ], 81 | [ 82 | 1471478400, 83 | 13021 84 | ], 85 | [ 86 | 1471564800, 87 | 12347 88 | ], 89 | [ 90 | 1471651200, 91 | 13854 92 | ], 93 | [ 94 | 1471737600, 95 | 10842 96 | ], 97 | [ 98 | 1471824000, 99 | 11820 100 | ], 101 | [ 102 | 1471910400, 103 | 13747 104 | ], 105 | [ 106 | 1471996800, 107 | 11961 108 | ], 109 | [ 110 | 1472083200, 111 | 11927 112 | ], 113 | [ 114 | 1472169600, 115 | 15068 116 | ], 117 | [ 118 | 1472256000, 119 | 11397 120 | ], 121 | [ 122 | 1472342400, 123 | 13610 124 | ], 125 | [ 126 | 1472428800, 127 | 13459 128 | ], 129 | [ 130 | 1472515200, 131 | 1009 132 | ] 133 | ], 134 | "24h": [ 135 | [ 136 | 1472432400, 137 | 519 138 | ], 139 | [ 140 | 1472436000, 141 | 808 142 | ], 143 | [ 144 | 1472439600, 145 | 710 146 | ], 147 | [ 148 | 1472443200, 149 | 225 150 | ], 151 | [ 152 | 1472446800, 153 | 392 154 | ], 155 | [ 156 | 1472450400, 157 | 137 158 | ], 159 | [ 160 | 1472454000, 161 | 846 162 | ], 163 | [ 164 | 1472457600, 165 | 912 166 | ], 167 | [ 168 | 1472461200, 169 | 843 170 | ], 171 | [ 172 | 1472464800, 173 | 336 174 | ], 175 | [ 176 | 1472468400, 177 | 747 178 | ], 179 | [ 180 | 1472472000, 181 | 638 182 | ], 183 | [ 184 | 1472475600, 185 | 656 186 | ], 187 | [ 188 | 1472479200, 189 | 511 190 | ], 191 | [ 192 | 1472482800, 193 | 884 194 | ], 195 | [ 196 | 1472486400, 197 | 549 198 | ], 199 | [ 200 | 1472490000, 201 | 163 202 | ], 203 | [ 204 | 1472493600, 205 | 546 206 | ], 207 | [ 208 | 1472497200, 209 | 496 210 | ], 211 | [ 212 | 1472500800, 213 | 868 214 | ], 215 | [ 216 | 1472504400, 217 | 419 218 | ], 219 | [ 220 | 1472508000, 221 | 296 222 | ], 223 | [ 224 | 1472511600, 225 | 690 226 | ], 227 | [ 228 | 1472515200, 229 | 466 230 | ], 231 | [ 232 | 1472518800, 233 | 543 234 | ] 235 | ] 236 | }, 237 | "culprit": "raven.scripts.runner in main", 238 | "title": "This is an example python exception", 239 | "pluginActions": [], 240 | "assignedTo": null, 241 | "participants": [], 242 | "logger": null, 243 | "type": "default", 244 | "annotations": [], 245 | "metadata": { 246 | "title": "This is an example python exception" 247 | }, 248 | "seenBy": [], 249 | "tags": [], 250 | "numComments": 0, 251 | "isPublic": false, 252 | "permalink": "https://app.getsentry.com/the-interstellar-jurisdiction/pump-station/issues/1/", 253 | "firstRelease": { 254 | "dateReleased": null, 255 | "url": null, 256 | "ref": null, 257 | "owner": null, 258 | "dateCreated": "2016-08-30T01:11:03.621Z", 259 | "lastEvent": "2016-08-30T01:11:03.700Z", 260 | "version": "7a7b2005c80710928dde1104db37ca0cdf9538fa", 261 | "firstEvent": "2016-08-30T01:11:03.700Z", 262 | "shortVersion": "7a7b2005c807", 263 | "dateStarted": null, 264 | "newGroups": 0, 265 | "data": {} 266 | }, 267 | "shortId": "PUMP-STATION-1", 268 | "shareId": "322e31", 269 | "firstSeen": "2016-08-30T01:11:03Z", 270 | "count": "1", 271 | "hasSeen": false, 272 | "level": "error", 273 | "isSubscribed": false, 274 | "isBookmarked": false, 275 | "project": { 276 | "name": "Pump Station", 277 | "slug": "pump-station" 278 | }, 279 | "lastRelease": null, 280 | "activity": [ 281 | { 282 | "type": "first_seen", 283 | "user": null, 284 | "data": {}, 285 | "id": "None", 286 | "dateCreated": "2016-08-30T01:11:03Z" 287 | } 288 | ], 289 | "statusDetails": {} 290 | } -------------------------------------------------------------------------------- /spec/fixtures/issue_events.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "eventID": "03fc4d526a3d47c7b2219ca1f3c81cf7", 4 | "sdk": null, 5 | "errors": [], 6 | "platform": "python", 7 | "contexts": {}, 8 | "size": 7911, 9 | "dateCreated": "2016-08-30T01:11:03Z", 10 | "dateReceived": "2016-08-30T01:11:03Z", 11 | "user": { 12 | "username": "getsentry", 13 | "email": "foo@example.com", 14 | "id": "1671" 15 | }, 16 | "context": { 17 | "emptyList": [], 18 | "unauthorized": false, 19 | "emptyMap": {}, 20 | "url": "http://example.org/foo/bar/", 21 | "results": [ 22 | 1, 23 | 2, 24 | 3, 25 | 4, 26 | 5 27 | ], 28 | "length": 10837790, 29 | "session": { 30 | "foo": "bar" 31 | } 32 | }, 33 | "entries": [ 34 | { 35 | "type": "message", 36 | "data": { 37 | "message": "This is an example python exception" 38 | } 39 | }, 40 | { 41 | "type": "stacktrace", 42 | "data": { 43 | "frames": [ 44 | { 45 | "function": "build_msg", 46 | "instructionOffset": null, 47 | "errors": null, 48 | "colNo": null, 49 | "module": "raven.base", 50 | "package": null, 51 | "absPath": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/base.py", 52 | "inApp": false, 53 | "instructionAddr": null, 54 | "filename": "raven/base.py", 55 | "platform": null, 56 | "vars": { 57 | "'frames'": "", 58 | "'culprit'": null, 59 | "'event_type'": "'raven.events.Message'", 60 | "'date'": "datetime.datetime(2013, 8, 13, 3, 8, 24, 880386)", 61 | "'extra'": { 62 | "'go_deeper'": [ 63 | [ 64 | { 65 | "'bar'": "'\\'\\\\\\'\\\\\\\\\\\\\\'[\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'baz\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\"]\\\\\\\\\\\\\\'\\\\\\'\\''", 66 | "'foo'": "'\\'\\\\\\'\\\\\\\\\\\\\\'\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'bar\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\"\\\\\\\\\\\\\\'\\\\\\'\\''" 67 | } 68 | ] 69 | ], 70 | "'user'": "'dcramer'", 71 | "'loadavg'": [ 72 | 0.37255859375, 73 | 0.5341796875, 74 | 0.62939453125 75 | ] 76 | }, 77 | "'v'": { 78 | "'message'": "u'This is a test message generated using ``raven test``'", 79 | "'params'": [] 80 | }, 81 | "'kwargs'": { 82 | "'message'": "'This is a test message generated using ``raven test``'", 83 | "'level'": 20 84 | }, 85 | "'event_id'": "'54a322436e1b47b88e239b78998ae742'", 86 | "'tags'": null, 87 | "'data'": { 88 | "'sentry.interfaces.Message'": { 89 | "'message'": "u'This is a test message generated using ``raven test``'", 90 | "'params'": [] 91 | }, 92 | "'message'": "u'This is a test message generated using ``raven test``'" 93 | }, 94 | "'self'": "", 95 | "'time_spent'": null, 96 | "'result'": { 97 | "'sentry.interfaces.Message'": { 98 | "'message'": "u'This is a test message generated using ``raven test``'", 99 | "'params'": [] 100 | }, 101 | "'message'": "u'This is a test message generated using ``raven test``'" 102 | }, 103 | "'stack'": true, 104 | "'handler'": "", 105 | "'k'": "'sentry.interfaces.Message'", 106 | "'public_key'": null 107 | }, 108 | "lineNo": 303, 109 | "context": [ 110 | [ 111 | 298, 112 | " frames = stack" 113 | ], 114 | [ 115 | 299, 116 | "" 117 | ], 118 | [ 119 | 300, 120 | " data.update({" 121 | ], 122 | [ 123 | 301, 124 | " 'sentry.interfaces.Stacktrace': {" 125 | ], 126 | [ 127 | 302, 128 | " 'frames': get_stack_info(frames," 129 | ], 130 | [ 131 | 303, 132 | " transformer=self.transform)" 133 | ], 134 | [ 135 | 304, 136 | " }," 137 | ], 138 | [ 139 | 305, 140 | " })" 141 | ], 142 | [ 143 | 306, 144 | "" 145 | ], 146 | [ 147 | 307, 148 | " if 'sentry.interfaces.Stacktrace' in data:" 149 | ], 150 | [ 151 | 308, 152 | " if self.include_paths:" 153 | ] 154 | ], 155 | "symbolAddr": null 156 | }, 157 | { 158 | "function": "capture", 159 | "instructionOffset": null, 160 | "errors": null, 161 | "colNo": null, 162 | "module": "raven.base", 163 | "package": null, 164 | "absPath": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/base.py", 165 | "inApp": false, 166 | "instructionAddr": null, 167 | "filename": "raven/base.py", 168 | "platform": null, 169 | "vars": { 170 | "'event_type'": "'raven.events.Message'", 171 | "'date'": null, 172 | "'extra'": { 173 | "'go_deeper'": [ 174 | [ 175 | { 176 | "'bar'": "'\\'\\\\\\'\\\\\\\\\\\\\\'[\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'baz\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\"]\\\\\\\\\\\\\\'\\\\\\'\\''", 177 | "'foo'": "'\\'\\\\\\'\\\\\\\\\\\\\\'\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'bar\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\"\\\\\\\\\\\\\\'\\\\\\'\\''" 178 | } 179 | ] 180 | ], 181 | "'user'": "'dcramer'", 182 | "'loadavg'": [ 183 | 0.37255859375, 184 | 0.5341796875, 185 | 0.62939453125 186 | ] 187 | }, 188 | "'stack'": true, 189 | "'tags'": null, 190 | "'data'": null, 191 | "'self'": "", 192 | "'time_spent'": null, 193 | "'kwargs'": { 194 | "'message'": "'This is a test message generated using ``raven test``'", 195 | "'level'": 20 196 | } 197 | }, 198 | "lineNo": 459, 199 | "context": [ 200 | [ 201 | 454, 202 | " if not self.is_enabled():" 203 | ], 204 | [ 205 | 455, 206 | " return" 207 | ], 208 | [ 209 | 456, 210 | "" 211 | ], 212 | [ 213 | 457, 214 | " data = self.build_msg(" 215 | ], 216 | [ 217 | 458, 218 | " event_type, data, date, time_spent, extra, stack, tags=tags," 219 | ], 220 | [ 221 | 459, 222 | " **kwargs)" 223 | ], 224 | [ 225 | 460, 226 | "" 227 | ], 228 | [ 229 | 461, 230 | " self.send(**data)" 231 | ], 232 | [ 233 | 462, 234 | "" 235 | ], 236 | [ 237 | 463, 238 | " return (data.get('event_id'),)" 239 | ], 240 | [ 241 | 464, 242 | "" 243 | ] 244 | ], 245 | "symbolAddr": null 246 | }, 247 | { 248 | "function": "captureMessage", 249 | "instructionOffset": null, 250 | "errors": null, 251 | "colNo": null, 252 | "module": "raven.base", 253 | "package": null, 254 | "absPath": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/base.py", 255 | "inApp": false, 256 | "instructionAddr": null, 257 | "filename": "raven/base.py", 258 | "platform": null, 259 | "vars": { 260 | "'message'": "'This is a test message generated using ``raven test``'", 261 | "'kwargs'": { 262 | "'extra'": { 263 | "'go_deeper'": [ 264 | [ 265 | "'\\'\\\\\\'\\\\\\\\\\\\\\'{\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'bar\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\": [\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'baz\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\"], \"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'foo\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\": \"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'bar\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\"}\\\\\\\\\\\\\\'\\\\\\'\\''" 266 | ] 267 | ], 268 | "'user'": "'dcramer'", 269 | "'loadavg'": [ 270 | 0.37255859375, 271 | 0.5341796875, 272 | 0.62939453125 273 | ] 274 | }, 275 | "'tags'": null, 276 | "'data'": null, 277 | "'level'": 20, 278 | "'stack'": true 279 | }, 280 | "'self'": "" 281 | }, 282 | "lineNo": 577, 283 | "context": [ 284 | [ 285 | 572, 286 | " \"\"\"" 287 | ], 288 | [ 289 | 573, 290 | " Creates an event from ``message``." 291 | ], 292 | [ 293 | 574, 294 | "" 295 | ], 296 | [ 297 | 575, 298 | " >>> client.captureMessage('My event just happened!')" 299 | ], 300 | [ 301 | 576, 302 | " \"\"\"" 303 | ], 304 | [ 305 | 577, 306 | " return self.capture('raven.events.Message', message=message, **kwargs)" 307 | ], 308 | [ 309 | 578, 310 | "" 311 | ], 312 | [ 313 | 579, 314 | " def captureException(self, exc_info=None, **kwargs):" 315 | ], 316 | [ 317 | 580, 318 | " \"\"\"" 319 | ], 320 | [ 321 | 581, 322 | " Creates an event from an exception." 323 | ], 324 | [ 325 | 582, 326 | "" 327 | ] 328 | ], 329 | "symbolAddr": null 330 | }, 331 | { 332 | "function": "send_test_message", 333 | "instructionOffset": null, 334 | "errors": null, 335 | "colNo": null, 336 | "module": "raven.scripts.runner", 337 | "package": null, 338 | "absPath": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/scripts/runner.py", 339 | "inApp": false, 340 | "instructionAddr": null, 341 | "filename": "raven/scripts/runner.py", 342 | "platform": null, 343 | "vars": { 344 | "'client'": "", 345 | "'options'": { 346 | "'tags'": null, 347 | "'data'": null 348 | }, 349 | "'data'": null, 350 | "'k'": "'secret_key'" 351 | }, 352 | "lineNo": 77, 353 | "context": [ 354 | [ 355 | 72, 356 | " level=logging.INFO," 357 | ], 358 | [ 359 | 73, 360 | " stack=True," 361 | ], 362 | [ 363 | 74, 364 | " tags=options.get('tags', {})," 365 | ], 366 | [ 367 | 75, 368 | " extra={" 369 | ], 370 | [ 371 | 76, 372 | " 'user': get_uid()," 373 | ], 374 | [ 375 | 77, 376 | " 'loadavg': get_loadavg()," 377 | ], 378 | [ 379 | 78, 380 | " }," 381 | ], 382 | [ 383 | 79, 384 | " ))" 385 | ], 386 | [ 387 | 80, 388 | "" 389 | ], 390 | [ 391 | 81, 392 | " if client.state.did_fail():" 393 | ], 394 | [ 395 | 82, 396 | " print('error!')" 397 | ] 398 | ], 399 | "symbolAddr": null 400 | }, 401 | { 402 | "function": "main", 403 | "instructionOffset": null, 404 | "errors": null, 405 | "colNo": null, 406 | "module": "raven.scripts.runner", 407 | "package": null, 408 | "absPath": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/scripts/runner.py", 409 | "inApp": false, 410 | "instructionAddr": null, 411 | "filename": "raven/scripts/runner.py", 412 | "platform": null, 413 | "vars": { 414 | "'root'": "", 415 | "'parser'": "", 416 | "'dsn'": "'https://ebc35f33e151401f9deac549978bda11:f3403f81e12e4c24942d505f086b2cad@app.getsentry.com/1'", 417 | "'opts'": "", 418 | "'client'": "", 419 | "'args'": [ 420 | "'test'", 421 | "'https://ebc35f33e151401f9deac549978bda11:f3403f81e12e4c24942d505f086b2cad@app.getsentry.com/1'" 422 | ] 423 | }, 424 | "lineNo": 112, 425 | "context": [ 426 | [ 427 | 107, 428 | " print(\"Using DSN configuration:\")" 429 | ], 430 | [ 431 | 108, 432 | " print(\" \", dsn)" 433 | ], 434 | [ 435 | 109, 436 | " print()" 437 | ], 438 | [ 439 | 110, 440 | "" 441 | ], 442 | [ 443 | 111, 444 | " client = Client(dsn, include_paths=['raven'])" 445 | ], 446 | [ 447 | 112, 448 | " send_test_message(client, opts.__dict__)" 449 | ] 450 | ], 451 | "symbolAddr": null 452 | } 453 | ], 454 | "framesOmitted": null, 455 | "hasSystemFrames": false 456 | } 457 | }, 458 | { 459 | "type": "template", 460 | "data": { 461 | "lineNo": 14, 462 | "context": [ 463 | [ 464 | 11, 465 | "{% endif %}\n" 466 | ], 467 | [ 468 | 12, 469 | "\n" 470 | ], 471 | [ 472 | 13, 473 | "