flipper-notifications 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7454bfa365f06fc55eca778468079bd424f5066d397793c4a00314a25fcba558
4
+ data.tar.gz: c20e47aee59098725c5d9997b4d0d0c6e580765a27e39077bd51a310ed5c7aaa
5
+ SHA512:
6
+ metadata.gz: f386c0fa4172a702549a7846480bef5a026852be8154dfb2a918f5024cf9eda5358bf910d63887b1a54b6e5483b17fc80be88b4cc388ace540ac7d6fdb787975
7
+ data.tar.gz: ee7c0d82fe98c5f826871792ffd3dda442a8a28e4fee84cc3267a6312ec4a8756d8104bf0058ffc90fb98d2fe063667c8f6d87d2a8925cf469792de82b4bdf23
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ *.swp
2
+ .DS_Store
3
+ /.bundle/
4
+ /.yardoc
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ /**/vendor/bundle/
12
+
13
+ # rspec failure tracking
14
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
4
+ --order rand
5
+ --exclude-pattern "spec/rails/**/*"
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ SuggestExtensions: false
4
+
5
+ Layout/HashAlignment:
6
+ EnforcedColonStyle: table
7
+ EnforcedHashRocketStyle: table
8
+
9
+ Lint/MissingSuper:
10
+ Enabled: false
11
+
12
+ Metrics/AbcSize:
13
+ Enabled: false
14
+
15
+ Metrics/BlockLength:
16
+ Enabled: false
17
+
18
+ Metrics/CyclomaticComplexity:
19
+ Enabled: false
20
+
21
+ Metrics/MethodLength:
22
+ Enabled: false
23
+
24
+ Style/Documentation:
25
+ Enabled: false
26
+
27
+ Style/HashSyntax:
28
+ EnforcedShorthandSyntax: never
29
+
30
+ Style/StringLiterals:
31
+ EnforcedStyle: double_quotes
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.2
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at joel.lubrano@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/jdlubrano/flipper-notifications" }
4
+
5
+ # Specify your gem's dependencies in flipper-notifications.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,117 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ flipper-notifications (0.1.0)
5
+ activesupport (~> 7.0)
6
+ flipper (~> 0.24)
7
+ httparty (~> 0.17)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activejob (7.0.4)
13
+ activesupport (= 7.0.4)
14
+ globalid (>= 0.3.6)
15
+ activesupport (7.0.4)
16
+ concurrent-ruby (~> 1.0, >= 1.0.2)
17
+ i18n (>= 1.6, < 2)
18
+ minitest (>= 5.1)
19
+ tzinfo (~> 2.0)
20
+ addressable (2.8.1)
21
+ public_suffix (>= 2.0.2, < 6.0)
22
+ ast (2.4.2)
23
+ bundler-gem_version_tasks (0.2.1)
24
+ concurrent-ruby (1.1.10)
25
+ crack (0.4.5)
26
+ rexml
27
+ debug (1.7.1)
28
+ irb (>= 1.5.0)
29
+ reline (>= 0.3.1)
30
+ diff-lcs (1.5.0)
31
+ docile (1.4.0)
32
+ flipper (0.26.0)
33
+ concurrent-ruby (< 2)
34
+ globalid (1.0.0)
35
+ activesupport (>= 5.0)
36
+ hashdiff (1.0.1)
37
+ httparty (0.20.0)
38
+ mime-types (~> 3.0)
39
+ multi_xml (>= 0.5.2)
40
+ i18n (1.12.0)
41
+ concurrent-ruby (~> 1.0)
42
+ io-console (0.6.0)
43
+ irb (1.6.2)
44
+ reline (>= 0.3.0)
45
+ json (2.6.3)
46
+ mime-types (3.4.1)
47
+ mime-types-data (~> 3.2015)
48
+ mime-types-data (3.2022.0105)
49
+ minitest (5.16.3)
50
+ multi_xml (0.6.0)
51
+ parallel (1.22.1)
52
+ parser (3.1.3.0)
53
+ ast (~> 2.4.1)
54
+ public_suffix (5.0.1)
55
+ rainbow (3.1.1)
56
+ rake (13.0.6)
57
+ regexp_parser (2.6.1)
58
+ reline (0.3.2)
59
+ io-console (~> 0.5)
60
+ rexml (3.2.5)
61
+ rspec (3.12.0)
62
+ rspec-core (~> 3.12.0)
63
+ rspec-expectations (~> 3.12.0)
64
+ rspec-mocks (~> 3.12.0)
65
+ rspec-core (3.12.0)
66
+ rspec-support (~> 3.12.0)
67
+ rspec-expectations (3.12.1)
68
+ diff-lcs (>= 1.2.0, < 2.0)
69
+ rspec-support (~> 3.12.0)
70
+ rspec-mocks (3.12.1)
71
+ diff-lcs (>= 1.2.0, < 2.0)
72
+ rspec-support (~> 3.12.0)
73
+ rspec-support (3.12.0)
74
+ rubocop (1.41.1)
75
+ json (~> 2.3)
76
+ parallel (~> 1.10)
77
+ parser (>= 3.1.2.1)
78
+ rainbow (>= 2.2.2, < 4.0)
79
+ regexp_parser (>= 1.8, < 3.0)
80
+ rexml (>= 3.2.5, < 4.0)
81
+ rubocop-ast (>= 1.23.0, < 2.0)
82
+ ruby-progressbar (~> 1.7)
83
+ unicode-display_width (>= 1.4.0, < 3.0)
84
+ rubocop-ast (1.24.0)
85
+ parser (>= 3.1.1.0)
86
+ ruby-progressbar (1.11.0)
87
+ simplecov (0.21.2)
88
+ docile (~> 1.1)
89
+ simplecov-html (~> 0.11)
90
+ simplecov_json_formatter (~> 0.1)
91
+ simplecov-html (0.12.3)
92
+ simplecov_json_formatter (0.1.4)
93
+ tzinfo (2.0.5)
94
+ concurrent-ruby (~> 1.0)
95
+ unicode-display_width (2.3.0)
96
+ webmock (3.18.1)
97
+ addressable (>= 2.8.0)
98
+ crack (>= 0.3.2)
99
+ hashdiff (>= 0.4.0, < 2.0.0)
100
+
101
+ PLATFORMS
102
+ arm64-darwin-21
103
+
104
+ DEPENDENCIES
105
+ activejob (~> 7.0)
106
+ bundler
107
+ bundler-gem_version_tasks
108
+ debug
109
+ flipper-notifications!
110
+ rake
111
+ rspec
112
+ rubocop
113
+ simplecov
114
+ webmock
115
+
116
+ BUNDLED WITH
117
+ 2.3.11
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Joel Lubrano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Flipper::Notifications
2
+
3
+ Rails-compatible Slack notifications when [Flipper](https://github.com/jnunemaker/flipper)
4
+ flags are updated.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'flipper-notifications'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install flipper-notifications
21
+
22
+ ## Dependencies
23
+
24
+ * Ruby 3
25
+ * ActiveSupport 7
26
+
27
+ This gem is designed to work within a Rails app. At the very least, you will
28
+ need `activesupport` since that library drives instrumentation from Flipper
29
+ itself.
30
+
31
+ ## Usage
32
+
33
+ After you initialize `Flipper`, you can also configure `Flipper::Notifications`.
34
+
35
+ ```ruby
36
+ # config/initializers/flipper.rb
37
+
38
+ Flipper.configure do |config|
39
+ config.adapter { ... }
40
+ end
41
+
42
+ Flipper::Notifications.configure do |config|
43
+ # You have to enable notifications; you probably only want notifications enabled in production.
44
+ config.enabled = true
45
+
46
+ slack_webhook = Flipper::Notifications::Webhooks::Slack.new(url: ENV.fetch("SLACK_WEBHOOK_URL"))
47
+
48
+ config.notifiers = [
49
+ Flipper::Notifications::Notifiers::WebhookNotifier.new(webhook: webhook)
50
+ ]
51
+ end
52
+ ```
53
+
54
+ ### Implementing Your Own Webhooks
55
+
56
+ WIP
57
+
58
+ ### Implementing Your Own Notifiers
59
+
60
+ A `Notifier` is any object that responds to a `call` method with a keyword
61
+ argument named `event`. You can use a `lambda` as your notifier if you prefer.
62
+ Using a `lambda` can come in handy if you want to provide additional context
63
+ to your notifications.
64
+
65
+ ```ruby
66
+ Flipper::Notifications.configure do |config|
67
+ webhook = Flipper::Notifications::Webhooks::Slack.new(url: ENV.fetch("SLACK_WEBHOOK_URL"))
68
+
69
+ notifier = ->(event:) do
70
+ context = "#{Rails.env} (changed by: #{Current.user.email})"
71
+ WebhookNotificationJob.perform_later(webhook: webhook, event: event, context_markdown: context)
72
+ end
73
+
74
+ config.notifiers = [notifier]
75
+ end
76
+ ```
77
+
78
+ ## Development
79
+
80
+ After checking out the repo, run `bin/setup` to install dependencies.
81
+ Then, run `rake spec` to run the tests. You can also run `bin/console` for an
82
+ interactive prompt that will allow you to experiment.
83
+
84
+ To install this gem onto your local machine, run `bundle exec rake install`.
85
+ To release a new version, update the version number in `version.rb`,
86
+ and then run `bundle exec rake release`, which will create a git tag for the
87
+ version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
88
+
89
+ ## Releasing
90
+
91
+ After merging in the new functionality to the main branch:
92
+
93
+ ```
94
+ git checkout main
95
+ git pull --prune
96
+ bundle exec rake version:bump:<major, minor, or patch>
97
+ bundle exec rubocop -a
98
+ git commit -a --amend
99
+ bundle exec rake release
100
+ ```
101
+
102
+ ## Contributing
103
+
104
+ Bug reports and pull requests are welcome on GitHub at
105
+ https://github.com/jdlubrano/flipper-notifications. This project is intended to
106
+ be a safe, welcoming space for collaboration, and contributors are expected to
107
+ adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
108
+
109
+ ## License
110
+
111
+ The gem is available as open source under the terms of the
112
+ [MIT License](https://opensource.org/licenses/MIT).
113
+
114
+ ## Code of Conduct
115
+
116
+ Everyone interacting in the Flipper::Notifications project’s codebases,
117
+ issue trackers, chat rooms and mailing lists is expected to follow the
118
+ [code of conduct](https://github.com/[USERNAME]/flipper-notifications/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "bundler/gem_version_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "flipper/notifications"
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(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "flipper/notifications/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "flipper-notifications"
9
+ spec.version = Flipper::Notifications::VERSION
10
+ spec.authors = ["Joel Lubrano"]
11
+ spec.email = ["joel.lubrano@gmail.com"]
12
+
13
+ spec.summary = %q{Rails-compatible Slack notifications for Flipper feature flags}
14
+ spec.homepage = "https://github.com/jdlubrano/flipper-notifications"
15
+ spec.license = "MIT"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+
27
+ spec.bindir = "bin"
28
+ spec.executables = []
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_runtime_dependency "activesupport", "~> 7.0"
32
+ spec.add_runtime_dependency "flipper", "~> 0.24"
33
+ spec.add_runtime_dependency "httparty", "~> 0.17"
34
+
35
+ spec.add_development_dependency "bundler"
36
+ spec.add_development_dependency 'bundler-gem_version_tasks'
37
+ spec.add_development_dependency "debug"
38
+ spec.add_development_dependency "rake"
39
+ spec.add_development_dependency "rspec"
40
+ spec.add_development_dependency "rubocop"
41
+ spec.add_development_dependency "simplecov"
42
+ spec.add_development_dependency "webmock"
43
+
44
+ spec.add_development_dependency "activejob", "~> 7.0"
45
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flipper
4
+ module Notifications
5
+ class Configuration
6
+ def initialize
7
+ @enabled = false
8
+ @notifiers = []
9
+ end
10
+
11
+ attr_accessor :enabled, :notifiers
12
+
13
+ def enabled?
14
+ @enabled
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job"
4
+ require_relative "feature_event"
5
+
6
+ module Flipper
7
+ module Notifications
8
+ class EventSerializer < ActiveJob::Serializers::ObjectSerializer
9
+ def serialize?(argument)
10
+ argument.is_a?(FeatureEvent)
11
+ end
12
+
13
+ def serialize(event)
14
+ super(
15
+ feature_name: event.feature.name,
16
+ operation: event.operation
17
+ )
18
+ end
19
+
20
+ def deserialize(hash)
21
+ FeatureEvent.new(**hash.symbolize_keys.slice(:feature_name, :operation))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flipper
4
+ module Notifications
5
+ class FeatureEvent
6
+ NOTEWORTHY_OPERATIONS = %w[
7
+ add
8
+ enable
9
+ disable
10
+ clear
11
+ remove
12
+ ].freeze
13
+
14
+ def self.from_active_support(event:)
15
+ new(
16
+ feature_name: event.payload[:feature_name],
17
+ operation: event.payload[:operation]
18
+ )
19
+ end
20
+
21
+ def initialize(feature_name:, operation:)
22
+ @feature = Flipper.feature(feature_name)
23
+ @operation = operation.to_s
24
+ end
25
+
26
+ attr_reader :feature, :operation
27
+
28
+ def summary_markdown
29
+ msg = String.new("Feature *#{feature.name}* was #{action_taken}.")
30
+
31
+ if include_state?
32
+ msg << " The feature is now *fully enabled.*" if feature.on?
33
+ msg << " The feature is now *fully disabled.*" if feature.off?
34
+ end
35
+
36
+ msg
37
+ end
38
+
39
+ def feature_enabled_settings_markdown
40
+ return "" unless feature.conditional?
41
+
42
+ [].tap do |settings|
43
+ settings << "The feature is now enabled for:" if feature.conditional?
44
+
45
+ settings << "- Groups: #{to_sentence(feature.enabled_groups.map(&:name).sort)}" if feature.enabled_groups.any?
46
+
47
+ settings << "- Users: #{to_sentence(feature.actors_value.sort)}" if feature.actors_value.any?
48
+
49
+ if feature.percentage_of_actors_value.positive?
50
+ settings << "- #{feature.percentage_of_actors_value}% of users"
51
+ end
52
+
53
+ settings << "- #{feature.percentage_of_time_value}% of the time" if feature.percentage_of_time_value.positive?
54
+ end.join("\n")
55
+ end
56
+
57
+ def noteworthy?
58
+ NOTEWORTHY_OPERATIONS.include?(operation)
59
+ end
60
+
61
+ def ==(other)
62
+ other.is_a?(self.class) && feature == other.feature && operation == other.operation
63
+ end
64
+
65
+ private
66
+
67
+ def action_taken
68
+ case operation
69
+ when "add"
70
+ "added"
71
+ when "clear"
72
+ "cleared"
73
+ when "remove"
74
+ "removed"
75
+ when "enable", "disable"
76
+ "updated"
77
+ else
78
+ "" # noop
79
+ end
80
+ end
81
+
82
+ def include_state?
83
+ %w[enable disable].include?(operation)
84
+ end
85
+
86
+ def to_sentence(words)
87
+ case words.length
88
+ when 0
89
+ ""
90
+ when 1
91
+ words.first.to_s
92
+ when 2
93
+ "#{words.first} and #{words.last}"
94
+ else
95
+ "#{words[0...-1].join(', ')} and #{words.last}"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+ require_relative "feature_event"
5
+
6
+ module Flipper
7
+ module Notifications
8
+ class FeaturesSubscriber
9
+ def call(*args)
10
+ return unless enabled?
11
+
12
+ event = FeatureEvent.from_active_support(event: ActiveSupport::Notifications::Event.new(*args))
13
+ Flipper::Notifications.notify(event: event) if event.noteworthy?
14
+ end
15
+
16
+ private
17
+
18
+ def enabled?
19
+ Flipper::Notifications.configuration.enabled?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job"
4
+ require "flipper/notifications/webhooks/errors"
5
+
6
+ module Flipper
7
+ module Notifications
8
+ class WebhookNotificationJob < ActiveJob::Base
9
+ # TODO: Pull queue from configuration?
10
+ # queue_as :low
11
+
12
+ retry_on Webhooks::NetworkError,
13
+ Webhooks::ServerError,
14
+ attempts: 3,
15
+ wait: :exponentially_longer
16
+
17
+ def perform(webhook:, **webhook_args)
18
+ webhook.notify(**webhook_args)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flipper/notifications/jobs/webhook_notification_job"
4
+
5
+ module Flipper
6
+ module Notifications
7
+ module Notifiers
8
+ class WebhookNotifier
9
+ def initialize(webhook:)
10
+ @webhook = webhook
11
+ end
12
+
13
+ def call(event:)
14
+ WebhookNotificationJob.perform_later(webhook: @webhook, event: event)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flipper/notifications/event_serializer"
4
+ require "flipper/notifications/webhooks/serializer"
5
+ require "flipper/notifications/notifiers/webhook_notifier"
6
+
7
+ module Flipper
8
+ module Notifications
9
+ class Railtie < Rails::Railtie
10
+ initializer "flipper-notifications.configure_rails_initialization" do
11
+ Flipper::Notifications.subscribe!
12
+
13
+ config.active_job.custom_serializers += [
14
+ EventSerializer,
15
+ Webhooks::Serializer
16
+ ]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flipper
4
+ module Notifications
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flipper
4
+ module Notifications
5
+ module Webhooks
6
+ class ApiError < StandardError
7
+ def initialize(response)
8
+ @response = response
9
+ end
10
+
11
+ def message
12
+ "Webhook API call resulted in #{@response.code} response: #{@response.body}"
13
+ end
14
+ end
15
+
16
+ class ClientError < ApiError; end
17
+
18
+ class ServerError < ApiError; end
19
+
20
+ class NetworkError < ApiError
21
+ def initialize(cause)
22
+ @cause = cause
23
+ end
24
+
25
+ def message
26
+ "Webhook API call network error: #{cause.class.name} - #{cause.message}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job"
4
+ require_relative "webhook"
5
+
6
+ module Flipper
7
+ module Notifications
8
+ module Webhooks
9
+ class Serializer < ActiveJob::Serializers::ObjectSerializer
10
+ def serialize?(argument)
11
+ argument.is_a?(Webhook)
12
+ end
13
+
14
+ def serialize(webhook)
15
+ super(
16
+ "class" => webhook.class.name,
17
+ "attributes" => webhook.serialized_attributes
18
+ )
19
+ end
20
+
21
+ def deserialize(hash)
22
+ hash["class"].constantize.new(**hash["attributes"].deep_symbolize_keys)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "webhook"
4
+
5
+ module Flipper
6
+ module Notifications
7
+ module Webhooks
8
+ class Slack < Webhook
9
+ MARKDOWN = "mrkdwn"
10
+
11
+ headers "Content-type" => "application/json"
12
+
13
+ def notify(event:, context_markdown: nil)
14
+ webhook_api_errors do
15
+ self.class.post(url, body: request_body(event: event, context_markdown: context_markdown))
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def request_body(event:, context_markdown:)
22
+ { blocks: blocks(event: event, context_markdown: context_markdown) }.to_json
23
+ end
24
+
25
+ def blocks(event:, context_markdown:)
26
+ [
27
+ feature_section(event: event),
28
+ context_block(context_markdown: context_markdown)
29
+ ].compact
30
+ end
31
+
32
+ def feature_section(event:)
33
+ {
34
+ type: "section",
35
+ text: {
36
+ type: MARKDOWN,
37
+ text: "#{event.summary_markdown}\n#{event.feature_enabled_settings_markdown}".strip
38
+ }
39
+ }
40
+ end
41
+
42
+ def context_block(context_markdown:)
43
+ return if context_markdown.nil?
44
+
45
+ {
46
+ type: "context",
47
+ elements: [
48
+ {
49
+ type: MARKDOWN,
50
+ text: context_markdown
51
+ }
52
+ ]
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+ require_relative "errors"
5
+
6
+ module Flipper
7
+ module Notifications
8
+ module Webhooks
9
+ class Webhook
10
+ include HTTParty
11
+
12
+ default_timeout 5 # seconds
13
+ raise_on 400..599
14
+
15
+ def initialize(url:)
16
+ @url = url
17
+ end
18
+
19
+ attr_reader :url
20
+
21
+ def notify(**_kwargs)
22
+ raise "Implement #notify in your subclass"
23
+ end
24
+
25
+ def serialized_attributes
26
+ { url: url }
27
+ end
28
+
29
+ def ==(other)
30
+ other.is_a?(self.class) && url == other.url
31
+ end
32
+
33
+ private
34
+
35
+ def webhook_api_errors(&block)
36
+ block.call
37
+ rescue HTTParty::ResponseError => e
38
+ error = e.response.code.to_i < 500 ? ClientError : ServerError
39
+ raise error, e.response
40
+ rescue Errno::ECONNRESET, Timeout::Error => e
41
+ raise NetworkError, e
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flipper
4
+ module Notifications
5
+ module Webhooks
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative "webhooks/slack"
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+ require "flipper/notifications/version"
5
+
6
+ require_relative "notifications/configuration"
7
+ require_relative "notifications/feature_event"
8
+ require_relative "notifications/features_subscriber"
9
+ require_relative "notifications/webhooks"
10
+
11
+ module Flipper
12
+ module Notifications
13
+ class Error < StandardError; end
14
+
15
+ module_function
16
+
17
+ @subscriber = nil
18
+
19
+ def configure
20
+ yield configuration if block_given?
21
+ end
22
+
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ def notify(event:)
28
+ configuration.notifiers.each { |notifier| notifier.call(event: event) }
29
+ end
30
+
31
+ def subscribe!
32
+ @subscriber = ActiveSupport::Notifications.subscribe(
33
+ Flipper::Feature::InstrumentationName,
34
+ FeaturesSubscriber.new
35
+ )
36
+ end
37
+
38
+ def unsubscribe!
39
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
40
+ end
41
+ end
42
+ end
43
+
44
+ require_relative "notifications/railtie" if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,241 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flipper-notifications
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joel Lubrano
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: flipper
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.24'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.24'
41
+ - !ruby/object:Gem::Dependency
42
+ name: httparty
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.17'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.17'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler-gem_version_tasks
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: debug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: webmock
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: activejob
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '7.0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '7.0'
181
+ description:
182
+ email:
183
+ - joel.lubrano@gmail.com
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - ".gitignore"
189
+ - ".rspec"
190
+ - ".rubocop.yml"
191
+ - ".ruby-version"
192
+ - CODE_OF_CONDUCT.md
193
+ - Gemfile
194
+ - Gemfile.lock
195
+ - LICENSE.txt
196
+ - README.md
197
+ - Rakefile
198
+ - bin/console
199
+ - bin/setup
200
+ - flipper-notifications.gemspec
201
+ - lib/flipper/notifications.rb
202
+ - lib/flipper/notifications/configuration.rb
203
+ - lib/flipper/notifications/event_serializer.rb
204
+ - lib/flipper/notifications/feature_event.rb
205
+ - lib/flipper/notifications/features_subscriber.rb
206
+ - lib/flipper/notifications/jobs/webhook_notification_job.rb
207
+ - lib/flipper/notifications/notifiers/webhook_notifier.rb
208
+ - lib/flipper/notifications/railtie.rb
209
+ - lib/flipper/notifications/version.rb
210
+ - lib/flipper/notifications/webhooks.rb
211
+ - lib/flipper/notifications/webhooks/errors.rb
212
+ - lib/flipper/notifications/webhooks/serializer.rb
213
+ - lib/flipper/notifications/webhooks/slack.rb
214
+ - lib/flipper/notifications/webhooks/webhook.rb
215
+ homepage: https://github.com/jdlubrano/flipper-notifications
216
+ licenses:
217
+ - MIT
218
+ metadata:
219
+ homepage_uri: https://github.com/jdlubrano/flipper-notifications
220
+ source_code_uri: https://github.com/jdlubrano/flipper-notifications
221
+ changelog_uri: https://github.com/jdlubrano/flipper-notifications/releases
222
+ post_install_message:
223
+ rdoc_options: []
224
+ require_paths:
225
+ - lib
226
+ required_ruby_version: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ version: '0'
231
+ required_rubygems_version: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
236
+ requirements: []
237
+ rubygems_version: 3.3.7
238
+ signing_key:
239
+ specification_version: 4
240
+ summary: Rails-compatible Slack notifications for Flipper feature flags
241
+ test_files: []