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
289 changes: 11 additions & 278 deletions lib/modularization_statistics/private/datadog_reporter.rb

Large diffs are not rendered by default.

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

module ModularizationStatistics
module Private
module Metrics
extend T::Sig
UNKNOWN_OWNER = T.let('Unknown', String)

sig { params(team_name: T.nilable(String)).returns(T::Array[Tag]) }
def self.tags_for_team(team_name)
[Tag.for('team', team_name || UNKNOWN_OWNER)]
end

sig { params(package: ParsePackwerk::Package, app_name: String).returns(T::Array[Tag]) }
def self.tags_for_package(package, app_name)
[
Tag.new(key: 'package', value: humanized_package_name(package.name)),
Tag.new(key: 'app', value: app_name),
*Metrics.tags_for_team(CodeOwnership.for_package(package)&.name),
]
end

sig { params(team_name: T.nilable(String)).returns(T::Array[Tag]) }
def self.tags_for_to_team(team_name)
[Tag.for('to_team', team_name || Metrics::UNKNOWN_OWNER)]
end

sig { params(name: String).returns(String) }
def self.humanized_package_name(name)
if name == ParsePackwerk::ROOT_PACKAGE_NAME
'root'
else
name
end
end

sig { params(violations: T::Array[ParsePackwerk::Violation]).returns(Integer) }
def self.file_count(violations)
violations.sum { |v| v.files.count }
end

sig { params(package: ParsePackwerk::Package).returns(T::Boolean) }
def self.has_readme?(package)
package.directory.join('README.md').exist?
end
end
end
end
47 changes: 47 additions & 0 deletions lib/modularization_statistics/private/metrics/files.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# typed: strict
# frozen_string_literal: true

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

sig do
params(
source_code_files: T::Array[SourceCodeFile],
app_name: String
).returns(T::Array[GaugeMetric])
end
def self.get_metrics(source_code_files, app_name)
all_metrics = T.let([], T::Array[GaugeMetric])
app_level_tag = Tag.for('app', app_name)

source_code_files.group_by { |file| file.team_owner&.name }.each do |team_name, files_for_team|
file_tags = Metrics.tags_for_team(team_name) + [app_level_tag]
all_metrics += get_file_metrics('by_team', file_tags, files_for_team)
end

file_tags = [app_level_tag]
all_metrics += get_file_metrics('totals', file_tags, source_code_files)
all_metrics
end

sig do
params(
metric_name_suffix: String,
tags: T::Array[Tag],
files: T::Array[SourceCodeFile]
).returns(T::Array[GaugeMetric])
end
def self.get_file_metrics(metric_name_suffix, tags, files)
[
GaugeMetric.for("component_files.#{metric_name_suffix}", files.count(&:componentized_file?), tags),
GaugeMetric.for("packaged_files.#{metric_name_suffix}", files.count(&:packaged_file?), tags),
GaugeMetric.for("all_files.#{metric_name_suffix}", files.count, tags),
]
end
end
end
end
end
109 changes: 109 additions & 0 deletions lib/modularization_statistics/private/metrics/packages.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# typed: strict
# frozen_string_literal: true

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

sig do
params(
packages: T::Array[ParsePackwerk::Package],
app_name: String
).returns(T::Array[GaugeMetric])
end
def self.get_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])
protected_packages = packages.map { |p| PackageProtections::ProtectedPackage.from(p) }

all_metrics << GaugeMetric.for('all_packages.count', packages.count, package_tags)
all_metrics << GaugeMetric.for('all_packages.dependencies.count', packages.sum { |package| package.dependencies.count }, package_tags)
all_metrics << GaugeMetric.for('all_packages.dependency_violations.count', protected_packages.sum { |package| Metrics.file_count(package.violations.select(&:dependency?)) }, package_tags)
all_metrics << GaugeMetric.for('all_packages.privacy_violations.count', protected_packages.sum { |package| Metrics.file_count(package.violations.select(&:privacy?)) }, package_tags)
all_metrics << GaugeMetric.for('all_packages.enforcing_dependencies.count', packages.count(&:enforces_dependencies?), package_tags)
all_metrics << GaugeMetric.for('all_packages.enforcing_privacy.count', packages.count(&:enforces_privacy?), package_tags)

all_metrics << GaugeMetric.for('all_packages.notify_on_package_yml_changes.count', packages.count { |p| p.metadata['notify_on_package_yml_changes'] }, package_tags)
all_metrics << GaugeMetric.for('all_packages.notify_on_new_violations.count', packages.count { |p| p.metadata['notify_on_new_violations'] }, package_tags)

all_metrics << GaugeMetric.for('all_packages.with_violations.count', protected_packages.count { |package| package.violations.any? }, package_tags)
all_metrics += Metrics::PublicUsage.get_public_usage_metrics('all_packages', packages, package_tags)
all_metrics << GaugeMetric.for('all_packages.has_readme.count', packages.count { |package| Metrics.has_readme?(package) }, package_tags)

all_metrics += Metrics::ProtectionUsage.get_protections_metrics('all_packages', protected_packages, package_tags)
all_metrics << GaugeMetric.for('all_packages.package_based_file_ownership.count', packages.count { |package| !package.metadata['owner'].nil? }, package_tags)

inbound_violations_by_package = protected_packages.flat_map(&:violations).group_by(&:to_package_name)

protected_packages.each do |protected_package|
package = protected_package.original_package
package_tags = Metrics.tags_for_package(package, app_name)

#
# VIOLATIONS (implicit dependencies)
#
outbound_violations = protected_package.violations
inbound_violations = inbound_violations_by_package[package.name] || []
all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?)
all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?)

all_metrics << GaugeMetric.for('by_package.dependency_violations.count', Metrics.file_count(all_dependency_violations), package_tags)
all_metrics << GaugeMetric.for('by_package.privacy_violations.count', Metrics.file_count(all_privacy_violations), package_tags)

all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), package_tags)
all_metrics << GaugeMetric.for('by_package.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), package_tags)

all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), package_tags)
all_metrics << GaugeMetric.for('by_package.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), package_tags)

all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_package', [package], package_tags)

protected_package.violations.group_by(&:to_package_name).each do |to_package_name, violations|
to_package = ParsePackwerk.find(to_package_name)
if to_package.nil?
raise StandardError, "Could not find matching package #{to_package_name}"
end

tags = package_tags + [Tag.for('to_package', Metrics.humanized_package_name(to_package_name))] + Metrics.tags_for_to_team(CodeOwnership.for_package(to_package)&.name)
all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.per_package.count', Metrics.file_count(violations.select(&:dependency?)), tags)
all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.per_package.count', Metrics.file_count(violations.select(&:privacy?)), tags)
end
end

inbound_explicit_dependency_by_package = {}
packages.each do |package|
package.dependencies.each do |explicit_dependency|
inbound_explicit_dependency_by_package[explicit_dependency] ||= []
inbound_explicit_dependency_by_package[explicit_dependency] << package.name
end
end

packages.each do |package| # rubocop:disable Style/CombinableLoops
package_tags = Metrics.tags_for_package(package, app_name)

#
# EXPLICIT DEPENDENCIES
#
package.dependencies.each do |explicit_dependency|
to_package = ParsePackwerk.find(explicit_dependency)
if to_package.nil?
raise StandardError, "Could not find matching package #{explicit_dependency}"
end

tags = package_tags + [Tag.for('to_package', Metrics.humanized_package_name(explicit_dependency))] + Metrics.tags_for_to_team(CodeOwnership.for_package(to_package)&.name)
all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.per_package.count', 1, tags)
end

all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.count', package.dependencies.count, package_tags)
all_metrics << GaugeMetric.for('by_package.inbound_explicit_dependencies.count', inbound_explicit_dependency_by_package[package.name]&.count || 0, package_tags)
end

all_metrics
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# typed: strict
# frozen_string_literal: true

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

sig do
params(
all_packages: T::Array[ParsePackwerk::Package],
app_name: String
).returns(T::Array[GaugeMetric])
end
def self.get_package_metrics_by_team(all_packages, app_name)
all_metrics = T.let([], T::Array[GaugeMetric])
app_level_tag = Tag.for('app', app_name)
all_protected_packages = all_packages.map { |p| PackageProtections::ProtectedPackage.from(p) }
all_protected_packages.group_by { |protected_package| CodeOwnership.for_package(protected_package.original_package)&.name }.each do |team_name, protected_packages_by_team|
# We look at `all_packages` because we care about ALL inbound violations across all teams
inbound_violations_by_package = all_protected_packages.flat_map(&:violations).group_by(&:to_package_name)

team_tags = Metrics.tags_for_team(team_name) + [app_level_tag]
all_metrics << GaugeMetric.for('by_team.all_packages.count', protected_packages_by_team.count, team_tags)
all_metrics += Metrics::ProtectionUsage.get_protections_metrics('by_team', protected_packages_by_team, team_tags)
all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_team', protected_packages_by_team.map(&:original_package), team_tags)

all_metrics << GaugeMetric.for('by_team.notify_on_package_yml_changes.count', protected_packages_by_team.count { |p| p.metadata['notify_on_package_yml_changes'] }, team_tags)
all_metrics << GaugeMetric.for('by_team.notify_on_new_violations.count', protected_packages_by_team.count { |p| p.metadata['notify_on_new_violations'] }, team_tags)

#
# VIOLATIONS (implicit dependencies)
#
outbound_violations = protected_packages_by_team.flat_map(&:violations)
# Here we only look at packages_by_team because we only care about inbound violations onto packages for this team
inbound_violations = protected_packages_by_team.flat_map { |package| inbound_violations_by_package[package.name] || [] }
all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?)
all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?)

all_metrics << GaugeMetric.for('by_team.dependency_violations.count', Metrics.file_count(all_dependency_violations), team_tags)
all_metrics << GaugeMetric.for('by_team.privacy_violations.count', Metrics.file_count(all_privacy_violations), team_tags)

all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), team_tags)
all_metrics << GaugeMetric.for('by_team.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), team_tags)

all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), team_tags)
all_metrics << GaugeMetric.for('by_team.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), team_tags)

all_metrics << GaugeMetric.for('by_team.has_readme.count', protected_packages_by_team.count { |protected_package| Metrics.has_readme?(protected_package.original_package) }, team_tags)

grouped_outbound_violations = outbound_violations.group_by do |violation|
to_package = ParsePackwerk.find(violation.to_package_name)
if to_package.nil?
raise StandardError, "Could not find matching package #{violation.to_package_name}"
end

CodeOwnership.for_package(to_package)&.name
end

grouped_outbound_violations.each do |to_team_name, violations|
tags = team_tags + Metrics.tags_for_to_team(to_team_name)
all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.per_team.count', Metrics.file_count(violations.select(&:dependency?)), tags)
all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.per_team.count', Metrics.file_count(violations.select(&:privacy?)), tags)
end
end

all_metrics
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# typed: strict
# frozen_string_literal: true

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

sig { params(prefix: String, protected_packages: T::Array[PackageProtections::ProtectedPackage], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) }
def self.get_protections_metrics(prefix, protected_packages, package_tags)
PackageProtections.all.flat_map do |protection|
PackageProtections::ViolationBehavior.each_value.map do |violation_behavior|
# https://github.com/Gusto/package_protections/pull/42 changed the public API of these violation behaviors.
# To preserve our ability to understand historical trends, we map to the old values.
# This allows our dashboards to continue to operate as expected.
# Note if we ever open source mod stats, we should probably inject this behavior so that new clients can see the new keys in their metrics.
violation_behavior_map = {
PackageProtections::ViolationBehavior::FailOnAny => 'fail_the_build_on_any_instances',
PackageProtections::ViolationBehavior::FailNever => 'no',
PackageProtections::ViolationBehavior::FailOnNew => 'fail_the_build_if_new_instances_appear',
}
violation_behavior_name = violation_behavior_map[violation_behavior]
metric_name = "#{prefix}.#{protection.identifier}.#{violation_behavior_name}.count"
count_of_packages = protected_packages.count { |p| p.violation_behavior_for(protection.identifier) == violation_behavior }
GaugeMetric.for(metric_name, count_of_packages, package_tags)
end
end
end
end
end
end
end
36 changes: 36 additions & 0 deletions lib/modularization_statistics/private/metrics/public_usage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# typed: strict
# frozen_string_literal: true

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

sig { params(prefix: String, packages: T::Array[ParsePackwerk::Package], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) }
def self.get_public_usage_metrics(prefix, packages, package_tags)
packages_except_for_root = packages.reject { |package| package.name == ParsePackwerk::ROOT_PACKAGE_NAME }
all_files = packages_except_for_root.flat_map do |package|
package.directory.glob('**/**.rb')
end

all_public_files = T.let([], T::Array[Pathname])
is_using_public_directory = 0
packages_except_for_root.each do |package|
public_files = package.directory.glob('app/public/**/**.rb')
all_public_files += public_files
is_using_public_directory += 1 if public_files.any?
end

# In Datadog, we can divide public files by all files to get the ratio.
# This is not a metric that we are targeting -- its for observability and reflection only.
[
GaugeMetric.for("#{prefix}.all_files.count", all_files.count, package_tags),
GaugeMetric.for("#{prefix}.public_files.count", all_public_files.count, package_tags),
GaugeMetric.for("#{prefix}.using_public_directory.count", is_using_public_directory, package_tags),
]
end
end
end
end
end