Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
733c69a
Start making more parts of the gem Rails version proof
azeveco May 22, 2025
7551dc9
Improve
azeveco May 22, 2025
346ee48
Improvements on version proof
azeveco May 22, 2025
bf6237c
Fixes comparison
azeveco May 22, 2025
b2ae437
Fixes comparison
azeveco May 22, 2025
80bd3cc
Test new insert_all
azeveco May 22, 2025
5a87447
Change model location
azeveco May 22, 2025
9c3a453
Implement insert_all for Rails older than 6
azeveco May 22, 2025
feacd2e
Improve assignable scope with_features
azeveco May 22, 2025
1cf0244
Fixes Robocop complexity offense
azeveco May 23, 2025
6076604
Ignore Rubocop offense
azeveco May 23, 2025
f7b24fb
Format
azeveco May 23, 2025
a53127f
Update Gemfile.lock
azeveco May 23, 2025
ab7caed
feature(analytics): first version tracking a single feature
cavalcanteyury May 24, 2025
aa6a22d
feature(analytics): created and documented main method to analyze fea…
cavalcanteyury May 24, 2025
db3c7a9
Merge branch 'main' into feature-analytics
cavalcanteyury May 24, 2025
ebcf2d2
docs(analytics): created analytics docs page in astro
cavalcanteyury May 25, 2025
a748aa9
docs(analytics): provided yard docs and rubocop fixes
cavalcanteyury May 25, 2025
885924a
tests(analytics): fixing tests warns
cavalcanteyury May 25, 2025
071a86d
tests(analytics): fixing reamining tests
cavalcanteyury May 25, 2025
4d3b080
docs(analytics): adding analytics docs to README
cavalcanteyury May 26, 2025
c61f5c4
docs(analytics): adding API reference for Analytics
cavalcanteyury May 26, 2025
fafe1d7
docs(analytics): removing unecessary note
cavalcanteyury May 26, 2025
dbce893
[FEATURE] Analytics first version (#44)
azeveco May 26, 2025
33b86a1
Merge branch 'dev' into version_proof
azeveco May 26, 2025
e1cdf1a
Make analytics version proof and add feature identifier to the return
azeveco May 26, 2025
9fd2d29
Adds new info to the analytics documentation
azeveco May 26, 2025
997882d
YARD doc for custom insert_all for Rails < 6
azeveco May 27, 2025
604a36b
Bump gem version
azeveco May 27, 2025
621e299
Improve the Version Proof of the Gem (#43)
azeveco May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 17 additions & 15 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,20 @@ GEM
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
bootsnap (1.18.4)
bootsnap (1.18.6)
msgpack (~> 1.2)
builder (3.3.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crass (1.0.6)
database_cleaner-active_record (2.2.0)
database_cleaner-active_record (2.2.1)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.4.1)
diff-lcs (1.6.1)
drb (2.2.1)
diff-lcs (1.6.2)
drb (2.2.3)
erb (5.0.1)
erubi (1.13.1)
globalid (1.2.1)
activesupport (>= 6.1)
Expand All @@ -104,11 +105,11 @@ GEM
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.11.3)
language_server-protocol (3.17.0.4)
json (2.12.0)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
Expand Down Expand Up @@ -154,12 +155,12 @@ GEM
prettyprint
prettyprint (0.2.0)
prism (1.4.0)
psych (5.2.3)
psych (5.2.6)
date
stringio
racc (1.8.1)
rack (3.1.13)
rack-session (2.1.0)
rack (3.1.15)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
Expand All @@ -180,7 +181,7 @@ GEM
activesupport (= 8.0.2)
bundler (>= 1.15.0)
railties (= 8.0.2)
rails-dom-testing (2.2.0)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
Expand All @@ -197,7 +198,8 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.13.1)
rdoc (6.14.0)
erb
psych (>= 4.0.0)
redcarpet (3.6.1)
regexp_parser (2.10.0)
Expand All @@ -212,7 +214,7 @@ GEM
rspec-expectations (3.13.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.3)
rspec-mocks (3.13.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.1.1)
Expand All @@ -224,7 +226,7 @@ GEM
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.3)
rubocop (1.75.4)
rubocop (1.75.7)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
Expand Down Expand Up @@ -263,7 +265,7 @@ GEM
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
yard (0.9.37)
zeitwerk (2.7.2)
zeitwerk (2.7.3)

PLATFORMS
aarch64-linux-gnu
Expand Down
78 changes: 76 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,81 @@ You want to list all features being used by assignables of User type:
Togglefy.for_type(User) # This returns all current FeatureAssignment with a User assignable
```

#### Aliases
## Analytics

So, you’ve been flipping features on and off like a light switch at a disco — but now you’re wondering:

> "How many people are actually using this shiny new feature I just toggled on?"

Or maybe:

> "Did I toggle something into oblivion and forget about it?"

Fear not — the Analytics module is here to save your (data-driven) soul.

#### Tracking a Feature usage data

You can track usage data for a single feature in your system by providing its `Togglefy::Feature` identifier:

```ruby
Togglefy.analytics_for(:teleportation) # :teleportation is the Feature's identifier column
```

This will return an array of hashes, with each hash representing an Assignable in your system (e.g., User, Account, Group, etc.).

Here’s what the result might look like:

```ruby
[
{
assignable: "User",
feature: :teleportation,
total: 50,
enabled_count: 20,
disabled_count: 30,
percentage_enabled: "40.0%",
percentage_disabled: "60.0%",
first_created: 2025-04-21 01:43:28.266664000 UTC +00:00,
last_created: 2025-05-21 19:18:37.772172000 UTC +00:00,
past_7: 9,
past_14: 11,
past_30: 0
},
{
assignable: "Account",
feature: :teleportation,
total: 4,
enabled_count: 2,
disabled_count: 0,
percentage_enabled: "100.0%",
percentage_disabled: "0.0%",
first_created: 2025-04-21 01:43:28.266664000 UTC +00:00,
last_created: 2025-05-21 19:18:37.772172000 UTC +00:00,
past_7: 2,
past_14: 0,
past_30: 0
}
]
```

So, how you should interpret that data?

Here’s what each field means:

* `assignable`: The model that includes Togglefy::Assignable.
* `feature`: The identifier of the feature being tracked (e.g., `:teleportation`).
* `total`: Total number of assignables in your system.
* `enabled_count`: Number of assignables that have the feature enabled.
* `disabled_count`: Number of assignables that have the feature disabled.
* `percentage_enabled`: Percentage of assignables with the feature enabled.
* `percentage_disabled`: Percentage of assignables with the feature disabled.
* `first_created`: The first time the feature was toggled on for this assignable.
* `last_created`: The most recent time the feature was toggled on for this assignable.
* `past_7`: Number of times the feature was toggled on in the past 7 days.
* `past_14`: Number of times the feature was toggled on in the past 14 days.
* `past_30`: Number of times the feature was toggled on in the past 30 days.

## Aliases

By the way, did you notice that I wrote `group` and `role` to get group?

Expand All @@ -452,7 +526,7 @@ Togglefy.for_filters(filters: {environment: :production})
Togglefy.for_filters(filter: {env: :production})
```

## Aliases table
#### Aliases table

Here's a table of all aliases available on Togglefy.

Expand Down
119 changes: 8 additions & 111 deletions app/models/togglefy/feature.rb
Original file line number Diff line number Diff line change
@@ -1,114 +1,11 @@
# frozen_string_literal: true

module Togglefy
# Represents a feature in the Togglefy system.
# A feature can have various attributes such as name, identifier, status, and associations with assignables.
class Feature < (defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base)
# Enum for feature status.
# If Rails version is 7 or higher, use the new enum syntax.
# If Rails version is lower, use the old enum syntax.
# @!attribute [r] status
# @return [Symbol] The status of the feature (:inactive or :active).
if Rails::VERSION::MAJOR >= 7
enum :status, %i[inactive active]
else
enum status: %i[inactive active]
end

# Associations
# @!attribute [rw] feature_assignments
# @return [ActiveRecord::Relation] The feature assignments associated with this feature.
has_many :feature_assignments, dependent: :destroy

# Callbacks
# Builds an identifier for the feature before validation if the name is present and identifier is blank.
before_validation :build_identifier, if: proc { |f| f.name.present? && f.identifier.blank? }

# Scopes
# Finds features by their identifier.
# @param identifier [Symbol, String, Array<Symbol, String>] The identifier to search for.
# @return [ActiveRecord::Relation] The features matching the identifier.
scope :identifier, ->(identifier) { where(identifier: identifier) }

# Finds features by their group.
# @param group [String] The group to search for.
# @return [ActiveRecord::Relation] The features matching the group.
scope :for_group, ->(group) { where(group: group) }

# Finds features without a group.
# @return [ActiveRecord::Relation] The features without a group.
scope :without_group, -> { where(group: nil) }

# Finds features by their environment.
# @param environment [String] The environment to search for.
# @return [ActiveRecord::Relation] The features matching the environment.
scope :for_environment, ->(environment) { where(environment: environment) }

# Finds features without an environment.
# @return [ActiveRecord::Relation] The features without an environment.
scope :without_environment, -> { where(environment: nil) }

# Finds features by their tenant ID.
# @param tenant_id [String] The tenant ID to search for.
# @return [ActiveRecord::Relation] The features matching the tenant ID.
scope :for_tenant, ->(tenant_id) { where(tenant_id: tenant_id) }

# Finds features without a tenant.
# @return [ActiveRecord::Relation] The features without a tenant.
scope :without_tenant, -> { where(tenant_id: nil) }

# Finds features with an inactive status.
# @return [ActiveRecord::Relation] The features with an inactive status.
scope :inactive, -> { where(status: :inactive) }

# Finds features with an active status.
# @return [ActiveRecord::Relation] The features with an active status.
scope :active, -> { where(status: :active) }

# Finds features by their status.
# @param status [Symbol, String, Integer] The status to search
# (:inactive || "inactive" || 0) or (:active || "active" || 1).
# @return [ActiveRecord::Relation] The features matching the status.
scope :with_status, ->(status) { where(status: status) }

# Validations
# Validates the presence and uniqueness of the name and identifier attributes.
validates :name, :identifier, presence: true, uniqueness: true
validates :identifier, format: {
with: /\A[a-z]+(_[a-z0-9]+)*\z/,
message: "must be in snake_case (lowercase letters and underscores only)"
}

# This method retrieves all assignables linked to the feature through feature assignments.
# @return [ActiveRecord::Relation] The assignables associated with the feature.
# @example
# feature.assignables
# Togglefy.feature(:super_powers).assignables
# Togglefy::Feature.find_by(identifier: :super_powers).assignables
# @note This method includes all assignables, regardless of their class.
def assignables
feature_assignments.includes(:assignable).map(&:assignable)
end

# This method retrieves assignables of a specific class linked to the feature through feature assignments.
# @param klass [String, Class] The class name or class of the assignable class.
# @return [ActiveRecord::Relation] The assignables of the specified class associated with the feature.
# @example
# feature.assignables_for_klass("User")
# feature.assignables_for_klass(User)
# Togglefy.feature(:super_powers).assignables_for_klass(User)
# Togglefy::Feature.find_by(identifier: :super_powers).assignables_for_klass(User)
def assignables_for_type(klass)
feature_assignments.includes(:assignable).where(assignable_type: klass.to_s).map(&:assignable)
end

private

# Builds a unique identifier for the feature based on its name.
# The identifier is generated by parameterizing the name with underscores.
# @return [void]
def build_identifier
self.identifier = name.underscore.parameterize(separator: "_")
end
end
ROOT_PATH = "../../.."

if Rails::VERSION::MAJOR >= 7
require_relative "#{ROOT_PATH}/lib/models/rails_7/feature"
elsif Rails::VERSION::MAJOR >= 5
require_relative "#{ROOT_PATH}/lib/models/rails_5/feature"
else
require_relative "#{ROOT_PATH}/lib/models/rails_legacy/feature"
end
25 changes: 7 additions & 18 deletions app/models/togglefy/feature_assignment.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
# frozen_string_literal: true

module Togglefy
# Represents the assignment of a feature to an assignable entity.
# A feature assignment links a feature to an assignable object, which can be of any polymorphic type.
class FeatureAssignment < ApplicationRecord
# Associations
# @!attribute [rw] feature
# @return [Feature] The feature associated with this assignment.
belongs_to :feature, class_name: "Togglefy::Feature"
ROOT_PATH = "../../.."

# @!attribute [rw] assignable
# @return [Object] The polymorphic assignable object associated with this assignment.
belongs_to :assignable, polymorphic: true

# Scopes
# Finds feature assignments for a specific assignable type.
# @param klass [Class] The class type of the assignable.
# @return [ActiveRecord::Relation] The feature assignments for the given type.
scope :for_type, ->(klass) { where(assignable_type: klass.to_s) }
end
if Rails::VERSION::MAJOR >= 7
require_relative "#{ROOT_PATH}/lib/models/rails_7/feature_assignment"
elsif Rails::VERSION::MAJOR >= 5
require_relative "#{ROOT_PATH}/lib/models/rails_5/feature_assignment"
else
require_relative "#{ROOT_PATH}/lib/models/rails_legacy/feature_assignment"
end
2 changes: 2 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default defineConfig({
{ label: 'Managing Features', slug: 'usage/managing-features' },
{ label: 'Managing Assignables <> Features', slug: 'usage/managing-assignables-features' },
{ label: 'Querying Features', slug: 'usage/querying-features' },
{ label: 'Analytics', slug: 'usage/analytics' },
{ label: 'Aliases', slug: 'usage/aliases' }
]
},
Expand Down Expand Up @@ -89,6 +90,7 @@ export default defineConfig({
{ label: 'Togglefy::FeatureManager', slug: 'reference/api/togglefy-feature-manager' },
{ label: 'Togglefy::FeatureAssignableManager', slug: 'reference/api/togglefy-feature-assignable-manager' },
{ label: 'Togglefy::ScopedBulkWrapper', slug: 'reference/api/togglefy-scoped-bulk-wrapper' },
{ label: 'Togglefy::Analytics', slug: 'reference/api/togglefy-analytics' }
]
},
{
Expand Down
Loading