govuk_content_item_loader 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 365987707cc39e8b636dfa88723bf0dd4c94a927b10188e72d2cc1a6db083abf
4
- data.tar.gz: 32de377938b8a4c3f7c5ed0ef369f4d18e67a807defb745ef1bf2d348db08266
3
+ metadata.gz: 2a533cf2cd72d38cfd19f7ef5555bb14c93156638ff6220d1dd494d92550a390
4
+ data.tar.gz: a1e9a897b7b82e747d81434c1d5965d550507edaa3a2a5ae4abde20df63e62ac
5
5
  SHA512:
6
- metadata.gz: '068e7bdf5420dac1daaebb68ceef011475c4ddd94b2204793c0e650909d4ae11c6606c8118d31980241e4fffcfdb2002a607cb3cb88ec8fcbced97ad580d8eb8'
7
- data.tar.gz: 695b64ce8e808861f42d95fa0e1516b326257dc0518dba887677e6619283d01a9dcdfcb0d59093b21b99a22613a6ac7b5a12c60c37c0ebbb4a8a52972d7cee96
6
+ metadata.gz: 85e6a82e33eb3fab62a93bfda0eeff17314f6ac79b4a6e9f2f09b138cbfa5c9c3812557b0638a5d294d9f28c790ee5f6b2e24293b28bd7042699e17c13b037b3
7
+ data.tar.gz: 7fd62a7aecc2bd6a214a92c6576a0469b3766d548ba699326f9798f7330d2f7b8fa5a219fca55fdac83e064aff2bd5a0e3ffe664ac1518ac6c8b36fe22af35f1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ - Add GraphQL traffic rates initializer.
6
+ - Add request-level conditional content item loader for GraphQL traffic routing with `load` and `can_load_from_graphql?` methods.
7
+
3
8
  ## 0.1.0
4
9
 
5
10
  Initial release
data/README.md CHANGED
@@ -2,6 +2,90 @@
2
2
 
3
3
  Ruby gem that standardises how GOV.UK frontend apps load content items. It provides configurable GraphQL traffic rates per content schema and a request-level loader that routes requests to the Publishing API via GraphQL or falls back to the Content Store ensuring consistent traffic management across applications.
4
4
 
5
+ ## Installation
6
+
7
+ Install the gem
8
+
9
+ `gem install govuk_content_item_loader`
10
+
11
+ or add it to your Gemfile
12
+
13
+ `gem "govuk_content_item_loader"`
14
+
15
+ ## GraphQL traffic rates
16
+
17
+ Provides a canonical way for frontend apps to configure GraphQL traffic rates for relevant content schemas. It sets the following application configuration:
18
+
19
+ * `Rails.application.config.graphql_allowed_schemas` - array of schema names that are eligible to be served via GraphQL.
20
+ * `Rails.application.config.graphql_traffic_rates` - hash mapping schema names to traffic percentages (as floats between 0 and 1.0).
21
+
22
+ Traffic rates for individual schemas are set in `govuk-helm-charts` repository as environment variables, with the following format `GRAPHQL_RATE_<SCHEMA_NAME>`.
23
+
24
+ ### Usage
25
+
26
+ To enable this functionality, create a file `config/initializers/govuk_graphql_traffic_rates.rb` in the app containing:
27
+
28
+ ```ruby
29
+ GovukGraphqlTrafficRates.configure
30
+ ```
31
+
32
+ ## Conditional Content Item Loader
33
+
34
+ This class acts as a traffic-splitting content loader that decides, per request, whether to fetch a content item from the **Publishing API via GraphQL** or fall back to the **Content Store**, based on request parameters and configured traffic-shaping rules based on content schemas.
35
+
36
+ **In practice, it:**
37
+
38
+ * Chooses between GraphQL (Publishing API) and the Content Store when loading a content item
39
+ * Always disables GraphQL on draft hosts
40
+ * Allows GraphQL to be explicitly forced on or off via the `graphql` request parameter
41
+ * Falls back to the Content Store unless the content item’s schema is explicitly allow-listed
42
+ * Uses a per-schema traffic rate to probabilistically route a percentage of requests to GraphQL
43
+ * Records GraphQL success, error status codes and timeouts in Prometheus request labels
44
+ * Propagates GdsApi errors
45
+
46
+ ### Usage
47
+
48
+ #### Prerequisites
49
+
50
+ 1. Ensure application configuration sets list of schema names that are eligible to be served via GraphQL and hash mapping schema names to traffic percentages via `GovukGraphqlTrafficRates` described above.
51
+
52
+ 2. Ensure the `PLEK_SERVICE_PUBLISHING_API_URI` environment variable is set in **govuk-helm-charts** for the live app (draft stack is handled via [`PLEK_HOSTNAME_PREFIX`](https://github.com/alphagov/plek/blob/main/CHANGELOG.md#410)), across all relevant environments, so the Publishing API (GraphQL) can be correctly resolved at runtime:
53
+
54
+ ```yaml
55
+ - name: PLEK_SERVICE_PUBLISHING_API_URI
56
+ value: "http://publishing-api-read-replica"
57
+ ```
58
+
59
+ 3. (Assumed true for all frontend apps) Ensure the application has Content Store connection configuration.
60
+
61
+ #### Using the loader
62
+
63
+ `GovukConditionalContentItemLoader` is intended to be used at the point where a request needs to load a content item, and where traffic may be conditionally routed to GraphQL instead of the Content Store.
64
+
65
+ At its simplest, you initialise the loader with the current request and call load:
66
+ ```
67
+ loader = GovukConditionalContentItemLoader.new(request: request)
68
+ content_item = loader.load
69
+ ```
70
+
71
+ By default, the loader uses:
72
+ * `GdsApi.content_store` to fetch content from the Content Store
73
+ * `GdsApi.publishing_api` to fetch content via GraphQL
74
+
75
+ These defaults make it suitable for use in production code without additional configuration.
76
+
77
+ *Passing custom clients*
78
+ For testing or specialised use cases, you can explicitly pass in the API clients:
79
+ ```
80
+ loader = GovukGraphql::ConditionalContentItemLoader.new(
81
+ request: request,
82
+ content_store_client: content_store_client,
83
+ publishing_api_client: publishing_api_client,
84
+ )
85
+ ```
86
+
87
+ The decision logic is also exposed via `can_load_from_graphql?`, allowing applications to make the routing decision themselves and implement custom fallback or error-handling behaviour if needed.
88
+
5
89
  ## Licence
6
90
 
7
91
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -20,7 +20,10 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = %w[lib]
21
21
 
22
22
  spec.add_dependency "gds-api-adapters", ">= 99.3"
23
+ spec.add_dependency "railties", ">= 7.2"
23
24
 
25
+ spec.add_development_dependency "climate_control"
26
+ spec.add_development_dependency "ostruct"
24
27
  spec.add_development_dependency "rails", ">= 7.2"
25
28
  spec.add_development_dependency "rake", ">= 10.0"
26
29
  spec.add_development_dependency "rspec", "~> 3.0"
@@ -0,0 +1,74 @@
1
+ class GovukConditionalContentItemLoader
2
+ attr_reader :content_store_client, :publishing_api_client, :request, :base_path
3
+
4
+ def initialize(request:, content_store_client: GdsApi.content_store, publishing_api_client: GdsApi.publishing_api)
5
+ @content_store_client = content_store_client
6
+ @publishing_api_client = publishing_api_client
7
+ @request = request
8
+ @base_path = request&.path
9
+ end
10
+
11
+ def load
12
+ can_load_from_graphql? ? content_item_from_graphql : content_item_from_content_store
13
+ end
14
+
15
+ def can_load_from_graphql?
16
+ return false unless request
17
+
18
+ return false if draft_host?
19
+
20
+ return force_graphql_param unless force_graphql_param.nil?
21
+
22
+ schema_name = content_item_from_content_store["schema_name"]
23
+ return false unless graphql_schema_allowed?(schema_name)
24
+
25
+ within_graphql_traffic_rate?(schema_name)
26
+ end
27
+
28
+ private
29
+
30
+ def content_item_from_graphql
31
+ set_prometheus_labels
32
+ publishing_api_client.graphql_live_content_item(base_path)
33
+ rescue GdsApi::HTTPErrorResponse => e
34
+ set_prometheus_labels(graphql_status_code: e.code)
35
+ raise e
36
+ rescue GdsApi::TimedOutException => e
37
+ set_prometheus_labels(graphql_api_timeout: true)
38
+ raise e
39
+ end
40
+
41
+ def content_item_from_content_store
42
+ @content_item_from_content_store ||= content_store_client.content_item(base_path)
43
+ end
44
+
45
+ def set_prometheus_labels(graphql_status_code: 200, graphql_api_timeout: false)
46
+ prometheus_labels = request.env.fetch("govuk.prometheus_labels", {})
47
+
48
+ hash = {
49
+ "graphql_status_code" => graphql_status_code,
50
+ "graphql_api_timeout" => graphql_api_timeout,
51
+ }
52
+
53
+ request.env["govuk.prometheus_labels"] = prometheus_labels.merge(hash)
54
+ end
55
+
56
+ def draft_host?
57
+ ENV["PLEK_HOSTNAME_PREFIX"] == "draft-"
58
+ end
59
+
60
+ def force_graphql_param
61
+ return true if request.params["graphql"] == "true"
62
+
63
+ false if request.params["graphql"] == "false"
64
+ end
65
+
66
+ def graphql_schema_allowed?(schema_name)
67
+ Rails.application.config.graphql_allowed_schemas&.include?(schema_name) || false
68
+ end
69
+
70
+ def within_graphql_traffic_rate?(schema_name)
71
+ graphql_traffic_rate = Rails.application.config.graphql_traffic_rates&.dig(schema_name) || 0
72
+ Random.rand(1.0) < graphql_traffic_rate
73
+ end
74
+ end
@@ -0,0 +1,15 @@
1
+ module GovukGraphqlTrafficRates
2
+ def self.configure
3
+ rates = graphql_rates_from_env
4
+
5
+ Rails.application.config.graphql_traffic_rates = rates
6
+ Rails.application.config.graphql_allowed_schemas = rates.keys
7
+ end
8
+
9
+ def self.graphql_rates_from_env
10
+ ENV
11
+ .select { |key, _| key.start_with?("GRAPHQL_RATE_") }
12
+ .transform_keys { |key| key.delete_prefix("GRAPHQL_RATE_").downcase }
13
+ .transform_values(&:to_f)
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module GovukContentItemLoader
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
@@ -1 +1,3 @@
1
+ require "govuk_content_item_loader/govuk_graphql_traffic_rates"
2
+ require "govuk_content_item_loader/govuk_conditional_content_item_loader"
1
3
  require "govuk_content_item_loader/version"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: govuk_content_item_loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GOV.UK Dev
@@ -23,6 +23,48 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '99.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: railties
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: climate_control
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ostruct
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
26
68
  - !ruby/object:Gem::Dependency
27
69
  name: rails
28
70
  requirement: !ruby/object:Gem::Requirement
@@ -117,6 +159,8 @@ files:
117
159
  - Rakefile
118
160
  - govuk_content_item_loader.gemspec
119
161
  - lib/govuk_content_item_loader.rb
162
+ - lib/govuk_content_item_loader/govuk_conditional_content_item_loader.rb
163
+ - lib/govuk_content_item_loader/govuk_graphql_traffic_rates.rb
120
164
  - lib/govuk_content_item_loader/version.rb
121
165
  homepage: https://github.com/alphagov/govuk_content_item_loader
122
166
  licenses: