Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
modularization_statistics (1.34.0)
modularization_statistics (1.35.0)
code_ownership
code_teams
dogapi
Expand Down
4 changes: 3 additions & 1 deletion lib/modularization_statistics/private/datadog_reporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require 'modularization_statistics/private/metrics/protection_usage'
require 'modularization_statistics/private/metrics/packages'
require 'modularization_statistics/private/metrics/packages_by_team'
require 'modularization_statistics/private/metrics/nested_packs'

module ModularizationStatistics
module Private
Expand All @@ -26,7 +27,8 @@ def self.get_metrics(source_code_files:, app_name:)
[
*Metrics::Files.get_metrics(source_code_files, app_name),
*Metrics::Packages.get_package_metrics(packages, app_name),
*Metrics::PackagesByTeam.get_package_metrics_by_team(packages, app_name)
*Metrics::PackagesByTeam.get_package_metrics_by_team(packages, app_name),
*Metrics::NestedPacks.get_nested_package_metrics(packages, app_name)
]
end

Expand Down
116 changes: 116 additions & 0 deletions lib/modularization_statistics/private/metrics/nested_packs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# typed: strict
# frozen_string_literal: true

module ModularizationStatistics
module Private
module Metrics
class NestedPacks
extend T::Sig

class PackGroup < T::Struct
extend T::Sig

const :name, String
const :root, ParsePackwerk::Package
const :members, T::Array[ParsePackwerk::Package]

sig { params(packages: T::Array[ParsePackwerk::Package]).returns(T::Array[PackGroup]) }
def self.all_from(packages)
packs_by_group = {}

packages.each do |package|
# For a child pack, package.directory is `packs/fruits/apples` (i.e. the directory of the package.yml file).
# The package.directory.dirname is therefore `packs/fruits`.
# For a standalone pack, package.directory.dirname is `packs`
# A pack with no parent is in a pack group of its own name
root = ParsePackwerk.find(package.directory.dirname.to_s) || package
# Mark the parent pack and child pack as being in the pack group of the parent
packs_by_group[root.name] ||= { root: root, members: [] }
packs_by_group[root.name][:members] << package
end

packs_by_group.map do |name, pack_data|
PackGroup.new(
name: name,
root: pack_data[:root],
members: pack_data[:members],
)
end
end

sig { returns(Integer) }
def children_pack_count
members.count do |package|
package.name != root.name
end
end

sig { returns(T::Boolean) }
def has_parent?
children_pack_count > 0
end

sig { returns(T::Array[ParsePackwerk::Violation]) }
def cross_group_violations
all_violations = members.flat_map do |member|
ParsePackwerk::DeprecatedReferences.for(member).violations
end

all_violations.select do |violation|
!members.map(&:name).include?(violation.to_package_name)
end
end
end

sig do
params(
packages: T::Array[ParsePackwerk::Package],
app_name: String
).returns(T::Array[GaugeMetric])
end
def self.get_nested_package_metrics(packages, app_name)
all_metrics = []
app_level_tag = Tag.for('app', app_name)
package_tags = T.let([app_level_tag], T::Array[Tag])

pack_groups = PackGroup.all_from(packages)
all_pack_groups_count = pack_groups.count
child_pack_count = pack_groups.sum(&:children_pack_count)
parent_pack_count = pack_groups.count(&:has_parent?)
all_cross_pack_group_violations = pack_groups.flat_map(&:cross_group_violations)

all_metrics << GaugeMetric.for('all_pack_groups.count', all_pack_groups_count, package_tags)
all_metrics << GaugeMetric.for('child_packs.count', child_pack_count, package_tags)
all_metrics << GaugeMetric.for('parent_packs.count', parent_pack_count, package_tags)
all_metrics << GaugeMetric.for('all_pack_groups.privacy_violations.count', Metrics.file_count(all_cross_pack_group_violations.select(&:privacy?)), package_tags)
all_metrics << GaugeMetric.for('all_pack_groups.dependency_violations.count', Metrics.file_count(all_cross_pack_group_violations.select(&:dependency?)), package_tags)

packs_by_group = {}
pack_groups.each do |pack_group|
pack_group.members.each do |member|
packs_by_group[member.name] = pack_group.name
end
end

pack_groups.each do |from_pack_group|
violations_by_to_pack_group = from_pack_group.cross_group_violations.group_by do |violation|
packs_by_group[violation.to_package_name]
end
violations_by_to_pack_group.each do |to_pack_group_name, violations|
tags = [
*package_tags,
Tag.for('pack_group', Metrics.humanized_package_name(from_pack_group.name)),
Tag.for('to_pack_group', Metrics.humanized_package_name(to_pack_group_name)),
]

all_metrics << GaugeMetric.for('by_pack_group.outbound_dependency_violations.per_pack_group.count', Metrics.file_count(violations.select(&:dependency?)), tags)
all_metrics << GaugeMetric.for('by_pack_group.outbound_privacy_violations.per_pack_group.count', Metrics.file_count(violations.select(&:privacy?)), tags)
end
end

all_metrics
end
end
end
end
end
2 changes: 1 addition & 1 deletion modularization_statistics.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Gem::Specification.new do |spec|
spec.name = 'modularization_statistics'
spec.version = '1.34.0'
spec.version = '1.35.0'
spec.authors = ['Gusto Engineers']
spec.email = ['dev@gusto.com']

Expand Down
119 changes: 113 additions & 6 deletions spec/modularization_statistics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
module ModularizationStatistics # rubocop:disable RSpec/DescribedClassModuleWrapping
RSpec.describe ModularizationStatistics do
before do
# We clear the packwerk cache so that we can test new sets of packages each time
# We should probably think of a better design to accomplish this!
ParsePackwerk.instance_variable_set(:@packages_by_name, nil)
ParsePackwerk.bust_cache!
end

describe 'ModularizationStatistics.report_to_datadog!' do
Expand Down Expand Up @@ -58,13 +56,10 @@ module ModularizationStatistics # rubocop:disable RSpec/DescribedClassModuleWrap
end

context 'in empty app' do
# This number will increase whenever a new protection is added
before do
write_file('empty_file.rb')
end

it { expect(metrics.count).to eq 35 }

it 'emits the right metrics' do
expect(metrics).to include_metric GaugeMetric.for('component_files.by_team', 0, Tags.for(['team:Unknown', 'app:MyApp']))
expect(metrics).to include_metric GaugeMetric.for('packaged_files.by_team', 0, Tags.for(['team:Unknown', 'app:MyApp']))
Expand Down Expand Up @@ -1011,6 +1006,118 @@ module ModularizationStatistics # rubocop:disable RSpec/DescribedClassModuleWrap
expect(metrics).to include_metric GaugeMetric.for('by_team.notify_on_new_violations.count', 1, Tags.for(['team:Unknown', 'app:MyApp']))
end
end

context 'in an app with nested packs' do
before do
write_package_yml('.')
write_package_yml('packs/fruits')
write_package_yml('packs/fruits/apples')
write_package_yml('packs/fruits/pears')

write_package_yml('packs/vegetables')
write_package_yml('packs/vegetables/broccoli')

write_package_yml('packs/peanuts')
write_package_yml('packs/cashews')

# Represents TWO privacy and TWO dependency violations across pack groups
write_file('deprecated_references.yml', <<~CONTENTS)
# This file contains a list of dependencies that are not part of the long term plan for ..
# We should generally work to reduce this list, but not at the expense of actually getting work done.
#
# You can regenerate this file using the following command:
#
# bundle exec packwerk update-deprecations .
---
packs/fruits:
"FruitsConstant":
violations:
- dependency
- privacy
files:
- some_file1.rb
- some_file2.rb
CONTENTS

# Represents ONE privacy and ZERO dependency violations across pack groups
write_file('packs/fruits/deprecated_references.yml', <<~CONTENTS)
# This file contains a list of dependencies that are not part of the long term plan for ..
# We should generally work to reduce this list, but not at the expense of actually getting work done.
#
# You can regenerate this file using the following command:
#
# bundle exec packwerk update-deprecations .
---
packs/fruits/apples:
"ApplesConstant":
violations:
- dependency
- privacy
files:
- some_file1.rb
- some_file2.rb
packs/peanuts:
"PeanutsConstant":
violations:
- privacy
files:
- some_file1.rb
CONTENTS

# Represents ZERO violations across pack groups
write_file('packs/fruits/apples/deprecated_references.yml', <<~CONTENTS)
# This file contains a list of dependencies that are not part of the long term plan for ..
# We should generally work to reduce this list, but not at the expense of actually getting work done.
#
# You can regenerate this file using the following command:
#
# bundle exec packwerk update-deprecations .
---
packs/fruits:
"FruitsConstant":
violations:
- dependency
- privacy
files:
- packs/fruits/apples/some_file1.rb
- packs/fruits/apples/some_file2.rb
- packs/fruits/apples/some_file3.rb
- packs/fruits/apples/some_file4.rb
packs/fruits/pears:
"PearsConstant":
violations:
- dependency
- privacy
files:
- packs/fruits/apples/some_file1.rb
- packs/fruits/apples/some_file2.rb
- packs/fruits/apples/some_file3.rb
- packs/fruits/apples/some_file4.rb
packs/peanuts:
"PeanutsConstant":
violations:
- dependency
files:
- packs/fruits/apples/some_file1.rb
CONTENTS
end

it 'emits the right metrics' do
expect(metrics).to include_metric GaugeMetric.for('all_packages.count', 8, Tags.for(['app:MyApp']))
expect(metrics).to include_metric GaugeMetric.for('all_pack_groups.count', 5, Tags.for(['app:MyApp']))
expect(metrics).to include_metric GaugeMetric.for('child_packs.count', 3, Tags.for(['app:MyApp']))
expect(metrics).to include_metric GaugeMetric.for('parent_packs.count', 2, Tags.for(['app:MyApp']))

# Notice that this does not use a tag to specify the pack group -- the metric itself only sends information about cross-pack group violations
expect(metrics).to include_metric GaugeMetric.for('all_pack_groups.privacy_violations.count', 3, Tags.for(['app:MyApp']))
expect(metrics).to include_metric GaugeMetric.for('all_pack_groups.dependency_violations.count', 3, Tags.for(['app:MyApp']))

# This does have a tag for pack group, but the metric itself also only sends information about cross-pack group violations.
expect(metrics).to include_metric GaugeMetric.for('by_pack_group.outbound_dependency_violations.per_pack_group.count', 2, Tags.for(['app:MyApp', 'pack_group:root', 'to_pack_group:packs/fruits']))
expect(metrics).to include_metric GaugeMetric.for('by_pack_group.outbound_privacy_violations.per_pack_group.count', 2, Tags.for(['app:MyApp', 'pack_group:root', 'to_pack_group:packs/fruits']))
expect(metrics).to include_metric GaugeMetric.for('by_pack_group.outbound_dependency_violations.per_pack_group.count', 1, Tags.for(['app:MyApp', 'pack_group:packs/fruits', 'to_pack_group:packs/peanuts']))
end
end
end
end
end
39 changes: 39 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,42 @@ def sorbet_double(stubbed_class, attr_map = {})
end
end
end

def write_package_yml(
pack_name,
dependencies: [],
enforce_dependencies: true,
enforce_privacy: true,
protections: {},
global_namespaces: [],
visible_to: [],
owner: nil
)
defaults = {
'prevent_this_package_from_violating_its_stated_dependencies' => 'fail_on_new',
'prevent_other_packages_from_using_this_packages_internals' => 'fail_on_new',
'prevent_this_package_from_exposing_an_untyped_api' => 'fail_on_new',
'prevent_this_package_from_creating_other_namespaces' => 'fail_on_new',
'prevent_other_packages_from_using_this_package_without_explicit_visibility' => 'fail_never',
}
protections_with_defaults = defaults.merge(protections)
metadata = { 'protections' => protections_with_defaults }

if owner
metadata.merge({ 'owner' => owner })
end

if global_namespaces
metadata.merge({ 'global_namespaces' => global_namespaces })
end

package = ParsePackwerk::Package.new(
name: pack_name,
dependencies: dependencies,
enforce_dependencies: enforce_dependencies,
enforce_privacy: enforce_privacy,
metadata: metadata
)

ParsePackwerk.write_package_yml!(package)
end