diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..fa7adc7 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.5 diff --git a/Gemfile b/Gemfile index 2d34af8..5227996 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,15 @@ gemspec group :development do gem "bundler", "~> 2" + gem "debug", ">= 1.0.0" + gem "rack-contrib", "~> 2" + gem "rack-test", "~> 2" gem "rake", "~> 13" gem "rspec", "~> 3" + gem "sinatra", "~> 2" + gem "sinatra-contrib" gem "standardrb", "~> 1" + gem "timecop", "~> 0.9" + gem "vcr", "~> 6" + gem "webmock", "~> 3" end diff --git a/Gemfile.lock b/Gemfile.lock index b457221..2bc9a28 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,22 +7,53 @@ PATH GEM remote: https://rubygems.org/ specs: + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) ast (2.4.2) base64 (0.2.0) + bigdecimal (3.1.8) + crack (1.0.0) + bigdecimal + rexml + debug (1.9.2) + irb (~> 1.10) + reline (>= 0.3.8) diff-lcs (1.5.1) graphql (2.3.7) base64 + hashdiff (1.1.1) + io-console (0.7.2) + irb (1.14.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) json (2.7.2) language_server-protocol (3.17.0.3) lint_roller (1.1.0) + multi_json (1.15.0) + mustermann (2.0.2) + ruby2_keywords (~> 0.0.1) parallel (1.26.3) parser (3.3.5.0) ast (~> 2.4.1) racc + psych (5.1.2) + stringio + public_suffix (6.0.1) racc (1.8.1) + rack (2.2.10) + rack-contrib (2.5.0) + rack (< 4) + rack-protection (2.2.4) + rack + rack-test (2.1.0) + rack (>= 1.3) rainbow (3.1.1) rake (13.2.1) + rdoc (6.7.0) + psych (>= 4.0.0) regexp_parser (2.9.2) + reline (0.5.10) + io-console (~> 0.5) rexml (3.3.8) rspec (3.13.0) rspec-core (~> 3.13.0) @@ -54,6 +85,18 @@ GEM rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + sinatra (2.2.4) + mustermann (~> 2.0) + rack (~> 2.2) + rack-protection (= 2.2.4) + tilt (~> 2.0) + sinatra-contrib (2.2.4) + multi_json + mustermann (~> 2.0) + rack-protection (= 2.2.4) + sinatra (= 2.2.4) + tilt (~> 2.0) standard (1.40.1) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) @@ -68,7 +111,16 @@ GEM rubocop-performance (~> 1.21.0) standardrb (1.0.1) standard + stringio (3.1.1) + tilt (2.4.0) + timecop (0.9.10) unicode-display_width (2.6.0) + vcr (6.3.1) + base64 + webmock (3.24.0) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) PLATFORMS arm64-darwin-23 @@ -76,10 +128,18 @@ PLATFORMS DEPENDENCIES bundler (~> 2) + debug (>= 1.0.0) graphql-hive! + rack-contrib (~> 2) + rack-test (~> 2) rake (~> 13) rspec (~> 3) + sinatra (~> 2) + sinatra-contrib standardrb (~> 1) + timecop (~> 0.9) + vcr (~> 6) + webmock (~> 3) BUNDLED WITH 2.5.15 diff --git a/README.md b/README.md index dad4305..e6501ca 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GraphQL Hive: `graphql-ruby` integration +# GraphQL Hive: `graphql-ruby` integration [![CI Suite](https://github.com/charlypoly/graphql-ruby-hive/actions/workflows/ci.yml/badge.svg)](https://github.com/charlypoly/graphql-ruby-hive/actions) [![Gem Version](https://badge.fury.io/rb/graphql-hive.svg)](https://rubygems.org/gems/graphql-hive) @@ -146,38 +146,53 @@ class MySchema < GraphQL::Schema use( GraphQL::Hive, { - # mandatory - token: 'YOUR-TOKEN', - - # optional - enabled: true, # enable/disable Hive Client - debug: false, # verbose logs + # token is the only required configuration value + token: 'YOUR-REGISTRY-TOKEN', + # + # The following are optional configuration values + # + # enable/disable Hive Client + enabled: true, + # verbose logs + debug: false, + # A custom logger logger: MyLogger.new, + # endpoint and port of the Hive API. Change this if you are using a self-hosted Hive instance endpoint: 'app.graphql-hive.com', port: 80, - buffer_size: 50, # how many operations can be sent to hive in a single batch (AFTER sampling) - - collect_usage: true, # report usage to Hive + # number of operations sent to hive in a batch (AFTER sampling) + buffer_size: 50, + # size of the queue used to send operations to the buffer before sampling + queue_size: 1000, + # report usage to hive + collect_usage: true, + # Usage sampling configurations collect_usage_sampling: { - # optional members of `collect_usage_sampling` - sample_rate: 0.5, # % of operations reported - sampler: proc { |context| context.operation_name.includes?('someQuery') 1 : 0.5 }, # assign custom sampling rates (overrides `sampling rate`) - at_least_once: true, # sample every distinct operation at least once - key_generator: proc { |context| context.operation_name } # assign custom keys to distinguish between distinct operations + # % of operations recorded + sample_rate: 0.5, + # custom sampler to assign custom sampling rates + sampler: proc { |context| context.operation_name.includes?('someQuery') 1 : 0.5 }, + # sample every distinct operation at least once + at_least_once: true, + # assign custom keys to distinguish between distinct operations + key_generator: proc { |context| context.operation_name } }, - report_schema: true, # publish schema to Hive + # publish schema to Hive + report_schema: true, # mandatory if `report_schema: true` - reporting: { + reporting: { # mandatory members of `reporting` author: 'Author of the latest change', commit: 'git sha or any identifier', - # optional members of `reporting + # optional members of `reporting service_name: '', service_url: '', }, # pass an optional proc to client_info to help identify the client (ex: Apollo web app) that performed the query - client_info: proc { |context| { name: context.client_name, version: context.client_version } } + client_info: proc { |context| + { name: context.client_name, version: context.client_version } + } } ) @@ -186,16 +201,14 @@ class MySchema < GraphQL::Schema end ``` -See default options for the optional parameters [here](https://github.com/charlypoly/graphql-ruby-hive/blob/01407d8fed80912a7006fee503bf2967fa20a79c/lib/graphql-hive.rb#L53). +See default options for the optional parameters [here](https://github.com/rperryng/graphql-ruby-hive/blob/master/lib/graphql-hive.rb#L31-L41).
-**A note on `buffer_size` and performances** - -The `graphql-hive` usage reporter, responsible for sending the operations data to Hive, is running in a separate `Thread` to avoid any significant impact on your GraphQL API performances. - -The performance overhead (with the default `buffer_size` option) is around 1% and [is constantly evaluated for new PR](https://github.com/charlypoly/graphql-ruby-hive/actions/workflows/benchmark.yml). - -If your GraphQL API has a high RPM, we encourage you to increase the `buffer_size` value. - -However, please note that a higher `buffer_size` value will introduce some peak of increase in memory consumption. +> [!Important] +> `buffer_size` and `queue_size` will affect memory consumption. +> +> `buffer_size` is the number of operations sent to Hive in a batch after operations have been sampled. +> `queue_size` is the size of the queue used to send operations to the buffer before sampling. +> Adjust these values according to your application's memory constraints and throughput. +> High throughput applications will need a larger `queue_size`. diff --git a/examples/simple-api/Gemfile b/examples/simple-api/Gemfile deleted file mode 100644 index 2a3ecf2..0000000 --- a/examples/simple-api/Gemfile +++ /dev/null @@ -1,8 +0,0 @@ -source "https://rubygems.org" - -gem "graphql" -gem "graphql-hive", path: "../../" -gem "puma" -gem "rack-contrib" -gem "sinatra" -gem "sinatra-contrib" diff --git a/examples/simple-api/Gemfile.lock b/examples/simple-api/Gemfile.lock deleted file mode 100644 index fb8290d..0000000 --- a/examples/simple-api/Gemfile.lock +++ /dev/null @@ -1,48 +0,0 @@ -PATH - remote: ../.. - specs: - graphql-hive (0.3.3) - graphql (~> 2.0.9) - -GEM - remote: https://rubygems.org/ - specs: - graphql (2.0.9) - multi_json (1.15.0) - mustermann (1.1.1) - ruby2_keywords (~> 0.0.1) - nio4r (2.5.8) - puma (5.6.4) - nio4r (~> 2.0) - rack (2.2.3.1) - rack-contrib (2.3.0) - rack (~> 2.0) - rack-protection (2.2.0) - rack - ruby2_keywords (0.0.5) - sinatra (2.2.0) - mustermann (~> 1.0) - rack (~> 2.2) - rack-protection (= 2.2.0) - tilt (~> 2.0) - sinatra-contrib (2.2.0) - multi_json - mustermann (~> 1.0) - rack-protection (= 2.2.0) - sinatra (= 2.2.0) - tilt (~> 2.0) - tilt (2.0.10) - -PLATFORMS - ruby - -DEPENDENCIES - graphql - graphql-hive! - puma - rack-contrib - sinatra - sinatra-contrib - -BUNDLED WITH - 2.5.15 diff --git a/examples/simple-api/app.rb b/examples/simple-api/app.rb deleted file mode 100644 index 7cef098..0000000 --- a/examples/simple-api/app.rb +++ /dev/null @@ -1,31 +0,0 @@ -require "sinatra" -require "sinatra/json" -require "rack/contrib" - -require_relative "schema" - -# Test query: -# -# query GetPost($input: [PostInput!]!) { -# post(input: $input, test: TEST1) { -# title -# myId: id -# } -# } - -class DemoApp < Sinatra::Base - use Rack::JSONBodyParser - - post "/graphql" do - result = Schema.execute( - params["query"], - variables: params[:variables], - operation_name: params[:operationName], - context: { - client_name: "GraphQL Client", - client_version: "1.0" - } - ) - json result - end -end diff --git a/examples/simple-api/config.ru b/examples/simple-api/config.ru deleted file mode 100644 index f15c789..0000000 --- a/examples/simple-api/config.ru +++ /dev/null @@ -1,2 +0,0 @@ -require "./app" -run DemoApp diff --git a/examples/simple-api/schema.rb b/examples/simple-api/schema.rb deleted file mode 100644 index 598aa79..0000000 --- a/examples/simple-api/schema.rb +++ /dev/null @@ -1,52 +0,0 @@ -require "graphql" -require "graphql-hive" - -module Types - class PostType < GraphQL::Schema::Object - description "A blog post" - field :id, ID, null: false - field :title, String, null: false - # fields should be queried in camel-case (this will be `truncatedPreview`) - field :truncated_preview, String, null: false - end -end - -class Types::PostInput < GraphQL::Schema::InputObject - description "Query Post arguments" - argument :id, ID, required: true -end - -class Types::TestEnum < GraphQL::Schema::Enum - value "TEST1" - value "TEST2" - value "TEST3" -end - -class QueryType < GraphQL::Schema::Object - description "The query root of this schema" - - # First describe the field signature: - field :post, Types::PostType, "Find a post by ID" do - argument :input, [Types::PostInput] - argument :test, Types::TestEnum - end - - # Then provide an implementation: - def post(input:, test:) - {id: 1, title: "GraphQL Hive with `graphql-ruby`", - truncated_preview: "Monitor operations, inspect your queries and publish your GraphQL schema with GraphQL Hive"} - end -end - -class Schema < GraphQL::Schema - query QueryType - - use( - GraphQL::Hive, - buffer_size: 2, - token: "YOUR_TOKEN", - debug: true, - reporting: {author: "Charly Poly", commit: "109bb1e748bae21bdfe663c0ffc7e830"}, - client_info: proc { |context| {name: context[:client_name], version: context[:client_version]} } - ) -end diff --git a/lib/graphql-hive.rb b/lib/graphql-hive.rb index a380bac..b222459 100644 --- a/lib/graphql-hive.rb +++ b/lib/graphql-hive.rb @@ -10,6 +10,7 @@ require "graphql-hive/sampler" require "graphql-hive/sampling/basic_sampler" require "graphql-hive/sampling/dynamic_sampler" +require "graphql" module GraphQL # GraphQL Hive usage collector and schema reporter @@ -36,6 +37,7 @@ class Hive < GraphQL::Tracing::PlatformTracing read_operations: true, report_schema: true, buffer_size: 50, + queue_size: 1000, logger: nil, collect_usage_sampling: 1.0 }.freeze diff --git a/lib/graphql-hive/usage_reporter.rb b/lib/graphql-hive/usage_reporter.rb index 90c21ea..685888c 100644 --- a/lib/graphql-hive/usage_reporter.rb +++ b/lib/graphql-hive/usage_reporter.rb @@ -21,7 +21,10 @@ def initialize(options, client) @client = client @options_mutex = Mutex.new @sampler = Sampler.new(options[:collect_usage_sampling], options[:logger]) # NOTE: logs for deprecated field - @queue = BoundedQueue.new(bound: options[:buffer_size], logger: options[:logger]) + @queue = BoundedQueue.new( + bound: options[:queue_size], + logger: options[:logger] + ) start_thread end diff --git a/spec/graphql/graphql-hive/usage_reporter_spec.rb b/spec/graphql/graphql-hive/usage_reporter_spec.rb index 7da5b81..92c7529 100644 --- a/spec/graphql/graphql-hive/usage_reporter_spec.rb +++ b/spec/graphql/graphql-hive/usage_reporter_spec.rb @@ -4,7 +4,7 @@ RSpec.describe GraphQL::Hive::UsageReporter do let(:usage_reporter_instance) { described_class.new(options, client) } - let(:options) { {logger: logger, buffer_size: buffer_size} } + let(:options) { {logger: logger, buffer_size: buffer_size, queue_size: 1000} } let(:logger) { instance_double("Logger") } let(:client) { instance_double("Hive::Client") } let(:buffer_size) { 1 } @@ -80,7 +80,8 @@ let(:options) do { logger: logger, - buffer_size: 1 + buffer_size: 1, + queue_size: 1000 } end diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb new file mode 100644 index 0000000..18ab7d8 --- /dev/null +++ b/spec/integration_spec.rb @@ -0,0 +1,111 @@ +require "spec_helper" +require_relative "test_app/app" +require "rack/test" + +RSpec.describe TestApp do + include Rack::Test::Methods + + def app + TestApp + end + + wait_for_reporting = lambda do |timeout, req_count| + Timeout.timeout(timeout) do + loop do + if WebMock::RequestRegistry.instance.requested_signatures.hash.size >= req_count + break + end + sleep 0.1 + end + end + rescue Timeout::Error + puts "Timed out waiting for reporting to finish." + end + + let(:usage_request_count) { 4 } + let(:query) do + <<~GQL + query GetPost($id: ID!){ + post(id: $id) { + title + id + } + } + GQL + end + + let(:request_body) do + { + query: query, + variables: {id: 1}, + operationName: "GetPost" + } + end + + after do + GraphQL::Hive.instance.on_exit + end + + it( + "posts data to hive", + :aggregate_failures, + :vcr + ) do + VCR.use_cassette( + "graphql-hive-integration", + allow_unused_http_interactions: false + ) do + 20.times do + post "/graphql", request_body + + expect(last_response).to be_ok + expect(JSON.parse(last_response.body)).to( + match( + "data" => {"post" => {"id" => "1", "title" => "GraphQL Hive with `graphql-ruby`"}} + ) + ) + end + end + + # NOTE: Reporting happens in a background thread. We give the background + # thread 1 second to finish reporting before moving on. + wait_for_reporting.call(1, usage_request_count) + + WebMock::RequestRegistry.instance.requested_signatures.each do |request_signature| + request_body = JSON.parse(request_signature.body) + expected_body = { + "size" => 5, + "map" => { + "92c5ca035dc4ee9a7347ffb368cd9ffb" => { + "fields" => ["Query", "Query.post", "ID", "Query.post.id", "Post", "Post.title", "Post.id"], + "operationName" => "GetPost", + "operation" => "query GetPost($id: ID!) {\n post(id: $id) {\n id\n title\n }\n}" + } + }, + "operations" => Array.new(5) { + { + "operationMapKey" => "92c5ca035dc4ee9a7347ffb368cd9ffb", + "timestamp" => be_a(Integer), + "execution" => {"ok" => true, "duration" => be_a(Integer), "errorsTotal" => 0} + } + } + } + expect(request_body).to include(expected_body) + end + + expect(WebMock).to have_requested(:post, "https://app.graphql-hive.com/usage") + .with( + body: anything, + headers: { + "Accept" => "*/*", + "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "Authorization" => "fake-token", + "Content-Type" => "application/json", + "Graphql-Client-Name" => "Hive Ruby Client", + "Graphql-Client-Version" => /[\d+\..]/, + "User-Agent" => /Hive@[\d+\..]/, + "X-Usage-Api-Version" => "2" + } + ).times(usage_request_count) + end +end diff --git a/spec/mock_apis/TestApp/posts_data_to_hive.yml b/spec/mock_apis/TestApp/posts_data_to_hive.yml new file mode 100644 index 0000000..d7f6da5 --- /dev/null +++ b/spec/mock_apis/TestApp/posts_data_to_hive.yml @@ -0,0 +1,251 @@ +--- +http_interactions: + - request: + method: post + uri: https://app.graphql-hive.com/usage + body: + encoding: UTF-8 + string: + '{"size":5,"map":{"92c5ca035dc4ee9a7347ffb368cd9ffb":{"fields":["Query","Query.post","ID","Query.post.id","Post","Post.title","Post.id"],"operationName":"GetPost","operation":"query + GetPost($id: ID!) {\n post(id: $id) {\n id\n title\n }\n}"}},"operations":[{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177146,"execution":{"ok":true,"duration":675999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177149,"execution":{"ok":true,"duration":256999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177150,"execution":{"ok":true,"duration":200000,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177150,"execution":{"ok":true,"duration":2840000,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177153,"execution":{"ok":true,"duration":196999,"errorsTotal":0}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Hive@0.4.3 + Authorization: + - fake-token + X-Usage-Api-Version: + - "2" + Content-Type: + - application/json + Graphql-Client-Name: + - Hive Ruby Client + Graphql-Client-Version: + - 0.4.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 31 Oct 2024 17:36:17 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + X-Envoy-Upstream-Service-Time: + - "5" + Set-Cookie: + - X-Contour-Session-Affinity="a0093bdb9ab204eb"; Path=/; HttpOnly + Vary: + - Accept-Encoding + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=eYLQ2q7Fd6xW%2FonL9EGivHwlZ%2FcYnFcm6o8dzMwMxaQNKDa2wJlkDKtF89jNmpgadjv1QK5vLi1TCAs7h6iUrtJ2Zc66tkmgt5bNn3WnTKCLkFTpkQunOR9JWFZ1im7upgK3TFQe"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + Cf-Ray: + - 8db5680bcb0ea27f-YUL + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=11780&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2345&recv_bytes=1939&delivery_rate=268732&cwnd=244&unsent_bytes=0&cid=7245de9051890998&ts=59&x=0" + body: + encoding: ASCII-8BIT + string: '{"id":"ffcb6fee-0350-4afc-b84e-94ad3899b592","operations":{"rejected":0,"accepted":5}}' + recorded_at: Thu, 31 Oct 2024 17:36:17 GMT + - request: + method: post + uri: https://app.graphql-hive.com/usage + body: + encoding: UTF-8 + string: + '{"size":5,"map":{"92c5ca035dc4ee9a7347ffb368cd9ffb":{"fields":["Query","Query.post","ID","Query.post.id","Post","Post.title","Post.id"],"operationName":"GetPost","operation":"query + GetPost($id: ID!) {\n post(id: $id) {\n id\n title\n }\n}"}},"operations":[{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177155,"execution":{"ok":true,"duration":242000,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177156,"execution":{"ok":true,"duration":215000,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177156,"execution":{"ok":true,"duration":194999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177157,"execution":{"ok":true,"duration":191999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177158,"execution":{"ok":true,"duration":192000,"errorsTotal":0}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Hive@0.4.3 + Authorization: + - fake-token + X-Usage-Api-Version: + - "2" + Content-Type: + - application/json + Graphql-Client-Name: + - Hive Ruby Client + Graphql-Client-Version: + - 0.4.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 31 Oct 2024 17:36:17 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + X-Envoy-Upstream-Service-Time: + - "0" + Set-Cookie: + - X-Contour-Session-Affinity="88adc31a49ef0838"; Path=/; HttpOnly + Vary: + - Accept-Encoding + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=MI0AAp%2FSjJHIUfwbfuOQ21IjJoaMg62v8KwS7VubKuw2O4YQr0MvQDwWxbXgasPJrn3tXtb838bjAmfbYJ9MITls2eDJb1upLSkckPMSN%2BTKOnToI9LWTHlWOmm3WoByrTubw5Hh"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + Cf-Ray: + - 8db5680c4bbb33ee-YUL + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=11571&sent=5&recv=7&lost=0&retrans=0&sent_bytes=2347&recv_bytes=1938&delivery_rate=307792&cwnd=250&unsent_bytes=0&cid=0f827004a506bf18&ts=56&x=0" + body: + encoding: ASCII-8BIT + string: '{"id":"31000631-075a-48a6-a0b0-f324cd0e85f5","operations":{"rejected":0,"accepted":5}}' + recorded_at: Thu, 31 Oct 2024 17:36:17 GMT + - request: + method: post + uri: https://app.graphql-hive.com/usage + body: + encoding: UTF-8 + string: + '{"size":5,"map":{"92c5ca035dc4ee9a7347ffb368cd9ffb":{"fields":["Query","Query.post","ID","Query.post.id","Post","Post.title","Post.id"],"operationName":"GetPost","operation":"query + GetPost($id: ID!) {\n post(id: $id) {\n id\n title\n }\n}"}},"operations":[{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177159,"execution":{"ok":true,"duration":184999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177160,"execution":{"ok":true,"duration":190999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177160,"execution":{"ok":true,"duration":191999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177161,"execution":{"ok":true,"duration":2338999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177164,"execution":{"ok":true,"duration":200000,"errorsTotal":0}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Hive@0.4.3 + Authorization: + - fake-token + X-Usage-Api-Version: + - "2" + Content-Type: + - application/json + Graphql-Client-Name: + - Hive Ruby Client + Graphql-Client-Version: + - 0.4.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 31 Oct 2024 17:36:17 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + X-Envoy-Upstream-Service-Time: + - "1" + Set-Cookie: + - X-Contour-Session-Affinity="a3ca202a6b9f3a80"; Path=/; HttpOnly + Vary: + - Accept-Encoding + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=xoKZ0I9dT8l5I%2BfurUfVjTqTlr9u1b0H2NVKPkQqhBVYMWcSjPTvQXBLiiKhisA5r44%2BiVVV0B%2BC7bs3uMag8cYDdW8oQHIV%2B2HhfB%2BfVYtxmQ3kLQijRfsXpR9CFIAyHdwpEQ6f"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + Cf-Ray: + - 8db5680ccb82a2c4-YUL + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=10641&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2347&recv_bytes=1939&delivery_rate=235781&cwnd=241&unsent_bytes=0&cid=8daa35148ceb356e&ts=63&x=0" + body: + encoding: ASCII-8BIT + string: '{"id":"15d445a6-6e61-4c09-beaf-09d259c2be0f","operations":{"rejected":0,"accepted":5}}' + recorded_at: Thu, 31 Oct 2024 17:36:17 GMT + - request: + method: post + uri: https://app.graphql-hive.com/usage + body: + encoding: UTF-8 + string: + '{"size":5,"map":{"92c5ca035dc4ee9a7347ffb368cd9ffb":{"fields":["Query","Query.post","ID","Query.post.id","Post","Post.title","Post.id"],"operationName":"GetPost","operation":"query + GetPost($id: ID!) {\n post(id: $id) {\n id\n title\n }\n}"}},"operations":[{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177165,"execution":{"ok":true,"duration":188999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177167,"execution":{"ok":true,"duration":652999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177172,"execution":{"ok":true,"duration":1209000,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177177,"execution":{"ok":true,"duration":303999,"errorsTotal":0}},{"operationMapKey":"92c5ca035dc4ee9a7347ffb368cd9ffb","timestamp":1730396177178,"execution":{"ok":true,"duration":211999,"errorsTotal":0}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Hive@0.4.3 + Authorization: + - fake-token + X-Usage-Api-Version: + - "2" + Content-Type: + - application/json + Graphql-Client-Name: + - Hive Ruby Client + Graphql-Client-Version: + - 0.4.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 31 Oct 2024 17:36:17 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + X-Envoy-Upstream-Service-Time: + - "0" + Set-Cookie: + - X-Contour-Session-Affinity="0c971cf207daa737"; Path=/; HttpOnly + Vary: + - Accept-Encoding + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=V%2FZL5t59%2B8QLNifv%2F3db7%2BG81EgGZyraqNHunnSFSqsAA9qPlby2tuC2rvC1gT%2FUum3Mj2Gt8Zz7pgEuR%2BL9QDwvbXH3q%2BlGBCquIg2p2EaEQ%2BKjJZW8GvWqR8uYeTZkDnIyti13"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + Cf-Ray: + - 8db5680d5bc7a24e-YUL + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=11326&sent=5&recv=7&lost=0&retrans=0&sent_bytes=2344&recv_bytes=1939&delivery_rate=246255&cwnd=243&unsent_bytes=0&cid=bcc97bddaede23fd&ts=53&x=0" + body: + encoding: ASCII-8BIT + string: '{"id":"26a8e735-e8c8-4d4c-9b63-b8afc1ca3b25","operations":{"rejected":0,"accepted":5}}' + recorded_at: Thu, 31 Oct 2024 17:36:17 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a2c39b7..6fc08b8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true require "bundler/setup" +require "debug" require "graphql" require "graphql-hive" +require "timecop" +require "vcr" +require "webmock/rspec" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure @@ -15,3 +19,9 @@ c.syntax = :expect end end +VCR.configure do |config| + config.cassette_library_dir = "spec/mock_apis" + config.hook_into :webmock + config.configure_rspec_metadata! + config.allow_http_connections_when_no_cassette = true +end diff --git a/spec/test_app/app.rb b/spec/test_app/app.rb new file mode 100644 index 0000000..188741a --- /dev/null +++ b/spec/test_app/app.rb @@ -0,0 +1,58 @@ +require "graphql" +require "graphql-hive" +require "rack/contrib" +require "sinatra" +require "sinatra/json" + +module Types + class PostType < GraphQL::Schema::Object + description "A blog post" + field :id, ID, null: false + field :title, String, null: false + field :truncated_preview, String, null: false + end +end + +class QueryType < GraphQL::Schema::Object + description "The query root of this schema" + + field :post, Types::PostType, "Find a post by ID" do + argument :id, ID, required: true + end + + def post(id:) + {id: 1, title: "GraphQL Hive with `graphql-ruby`"} + end +end + +class Schema < GraphQL::Schema + query QueryType + + use( + GraphQL::Hive, + enabled: true, + token: "fake-token", + report_schema: false, + collect_usage_sampling: { + sample_rate: 1 + }, + buffer_size: 5 + ) +end + +class TestApp < Sinatra::Base + use Rack::JSONBodyParser + + post "/graphql" do + result = Schema.execute( + params["query"], + variables: params[:variables], + operation_name: params[:operationName], + context: { + client_name: "GraphQL Client", + client_version: "1.0" + } + ) + json result + end +end