pack_stats 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 60606e6b527c76b27deaac01ad5991ebe06ec7db76bc5fe9eb707d0cfdde9c56
4
+ data.tar.gz: ec768521fe361499a52bb29b59b97d23249ef9bd6a9c1bc3c543c8d46d6ccc08
5
+ SHA512:
6
+ metadata.gz: f33cff3e350f90dbd71cf07433df45bfc170fd9c2101e5713cae1976e034385af98fbc76eddc01ac0ed78009f6e23f2b2f43797999d1fd6e8405fbdb50c013b1
7
+ data.tar.gz: f04b2b9ac1e3cdf3fcdf0811a1d2a9d1b0a8b2864278ed892caf61c1676fcf762839d24f03ef0f7625895ec37287dcaf3cc034c7ab672468d9b5b1ccf18a15a6
data/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # PackStats
2
+
3
+ This gem is used to report opinionated statistics about modularization to DataDog and other observability systems.
4
+
5
+ # Configuring Ownership
6
+ The gem reports metrics per-team, where each team is configured based on metadata included in Packwerk package.yml files.
7
+
8
+ Define your teams as described in the [Code Team - Package Based Ownership](https://github.com/rubyatscale/code_ownership#package-based-ownership) documentation.
9
+
10
+ # Usage
11
+ The main method to this gem is `PackStats#report_to_datadog!`. Refer to the Sorbet signature for this method for the exact types to be passed in.
12
+
13
+ This is an example of how to use this API:
14
+
15
+ ```ruby
16
+ PackStats.report_to_datadog!(
17
+ #
18
+ # A properly initialized `Dogapi::Client`
19
+ # Example: Dogapi::Client.new(ENV.fetch('DATADOG_API_KEY')
20
+ #
21
+ datadog_client: datadog_client,
22
+ #
23
+ # Time attached to the metrics
24
+ # Example: Time.now
25
+ #
26
+ report_time: report_time
27
+ #
28
+ # This is used to determine what files to look at for building statistics about what types of files are packaged, componentized, or unpackaged.
29
+ # This is an array of `Pathname`. `Pathname` can be relative or absolute paths.
30
+ #
31
+ # Example: source_code_pathnames = Pathname.glob('./**/**.rb')
32
+ #
33
+ source_code_pathnames: source_code_pathnames,
34
+ #
35
+ # A file is determined to be componentized if it exists in any of these directories.
36
+ # This is an array of `Pathname`. `Pathname` can be relative or absolute paths.
37
+ #
38
+ # Example: [Pathname.new("./gems")]
39
+ #
40
+ componentized_source_code_locations: componentized_source_code_locations,
41
+ #
42
+ # A file is determined to be packaged if it exists in any of these directories.
43
+ # This is an array of `Pathname`. `Pathname` can be relative or absolute paths.
44
+ #
45
+ # Example: [Pathname.new("./packs")]
46
+ #
47
+ packaged_source_code_locations: packaged_source_code_locations,
48
+ )
49
+ ```
50
+
51
+ It's recommended to run this in CI on the main/development branch so each new commit has metrics emitted for it.
52
+
53
+ # Tracking Privacy and Dependency Violations Reliably
54
+ With [`packwerk`](https://github.com/Shopify/packwerk), privacy and dependency violations do not show up until a package has set `enforce_privacy` and `enforce_dependency` (respectively) to `true`. As such, when you're first starting off, you'll see no violations, and then periodic large increases as teams start using these protections. If you're interested in looking at privacy and dependency violations over time as if all packages were enforcing dependencies and privacy the whole time, we recommend setting these values to be true before running modularization statistics in your CI.
55
+
56
+ ```ruby
57
+ require 'pack_stats'
58
+
59
+ namespace(:modularization) do
60
+ desc(
61
+ 'Publish modularization stats to datadog. ' \
62
+ 'Example: bin/rails "modularization:upload_statistics"'
63
+ )
64
+ task(:upload_statistics, [:verbose] => :environment) do |_, args|
65
+ ignored_paths = Pathname.glob('spec/fixtures/**/**')
66
+ source_code_pathnames = Pathname.glob('{app,components,lib,packs,spec}/**/**').select(&:file?) - ignored_paths
67
+
68
+ # To correctly track violations, we rewrite all `package.yml` files with
69
+ # `enforce_dependencies` and `enforce_privacy` set to true, then update deprecations.
70
+ old_packages = ParsePackwerk.all
71
+ old_packages.each do |package|
72
+ new_package = ParsePackwerk::Package.new(
73
+ dependencies: package.dependencies,
74
+ enforce_dependencies: true,
75
+ enforce_privacy: true,
76
+ metadata: package.metadata,
77
+ name: package.name
78
+ )
79
+ ParsePackwerk.write_package_yml!(new_package)
80
+ end
81
+
82
+ Packwerk::Cli.new.execute_command(['update-deprecations'])
83
+
84
+ # Now we reset it back so that the protection values are the same as the native packwerk configuration
85
+ old_packages.each do |package|
86
+ new_package = ParsePackwerk::Package.new(
87
+ dependencies: package.dependencies,
88
+ enforce_dependencies: package.enforce_dependencies,
89
+ enforce_privacy: package.enforce_privacy,
90
+ metadata: package.metadata,
91
+ name: package.name
92
+ )
93
+ ParsePackwerk.write_package_yml!(new_package)
94
+ end
95
+
96
+ PackStats.report_to_datadog!(
97
+ datadog_client: Dogapi::Client.new(ENV.fetch('DATADOG_API_KEY')),
98
+ app_name: Rails.application.class.module_parent_name,
99
+ source_code_pathnames: source_code_pathnames,
100
+ verbose: args[:verbose] == 'true' || false
101
+ )
102
+ end
103
+ end
104
+ ```
105
+
106
+ # Using Other Observability Tools
107
+
108
+ Right now this tool sends metrics to DataDog early. However, if you want to use this with other tools, you can call `PackStats.get_metrics(...)` to get generic metrics that you can then send to whatever observability provider you use.
109
+
110
+ # Setting Up Your Dashboards
111
+
112
+ Gusto has two dashboards that we've created to view these metrics. We've also exported and released the Dashboard JSON for each of these dashboards. You can create a new dashboard and then click "import dashboard JSON" to get a jump start on tracking your metrics. Note you may want to make some tweaks to these dashboards to better fit your organization's circumstances and goals.
113
+
114
+ ## [Modularization] Executive Summary
115
+
116
+ This helps answer questions like:
117
+ - How are we doing on reducing dependency and privacy violations in your monolith overall?
118
+ - How are we doing overall on adopting package protections?
119
+
120
+ [Dashboard JSON](docs/executive_summary.json)
121
+
122
+ ## [Modularization] Per-Package and Per-Team
123
+ - How is each team and package doing on reducing dependency and privacy violations in your monolith?
124
+ - What is the total count of dependency/privacy violations for each pack/team and what's the change since last month?
125
+ - Which pack/team does my pack/team have the most dependency/privacy violations on?
126
+
127
+ [Dashboard JSON](docs/per_package_and_per_team.json)
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+
3
+ module PackStats
4
+ class GaugeMetric < T::Struct
5
+ extend T::Sig
6
+
7
+ const :name, String
8
+ const :count, Integer
9
+ const :tags, T::Array[Tag]
10
+
11
+ sig { params(metric_name: String, count: Integer, tags: T::Array[Tag]).returns(GaugeMetric) }
12
+ def self.for(metric_name, count, tags)
13
+ name = "modularization.#{metric_name}"
14
+ # https://docs.datadoghq.com/metrics/custom_metrics/#naming-custom-metrics
15
+ # Metric names must not exceed 200 characters. Fewer than 100 is preferred from a UI perspective
16
+ if name.length > 200
17
+ raise StandardError.new("Metrics names must not exceed 200 characters: #{name}") # rubocop:disable Style/RaiseArgs
18
+ end
19
+
20
+ new(
21
+ name: name,
22
+ count: count,
23
+ tags: tags
24
+ )
25
+ end
26
+
27
+ sig { returns(String) }
28
+ def to_s
29
+ "#{name} with count #{count}, with tags #{tags.map(&:to_s).join(', ')}"
30
+ end
31
+
32
+ sig { params(other: GaugeMetric).returns(T::Boolean) }
33
+ def ==(other)
34
+ other.name == self.name &&
35
+ other.count == self.count &&
36
+ other.tags == self.tags
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,61 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'dogapi'
5
+ require 'pack_stats/private/metrics'
6
+ require 'pack_stats/private/metrics/files'
7
+ require 'pack_stats/private/metrics/public_usage'
8
+ require 'pack_stats/private/metrics/packwerk_checker_usage'
9
+ require 'pack_stats/private/metrics/rubocop_usage'
10
+ require 'pack_stats/private/metrics/packages'
11
+ require 'pack_stats/private/metrics/packages_by_team'
12
+ require 'pack_stats/private/metrics/nested_packs'
13
+
14
+ module PackStats
15
+ module Private
16
+ class DatadogReporter
17
+ extend T::Sig
18
+
19
+ sig do
20
+ params(
21
+ source_code_files: T::Array[SourceCodeFile],
22
+ app_name: String
23
+ ).returns(T::Array[GaugeMetric])
24
+ end
25
+ def self.get_metrics(source_code_files:, app_name:)
26
+ packages = ParsePackwerk.all
27
+
28
+ [
29
+ *Metrics::Files.get_metrics(source_code_files, app_name),
30
+ *Metrics::Packages.get_package_metrics(packages, app_name),
31
+ *Metrics::PackagesByTeam.get_package_metrics_by_team(packages, app_name),
32
+ *Metrics::NestedPacks.get_nested_package_metrics(packages, app_name)
33
+ ]
34
+ end
35
+
36
+ sig do
37
+ params(
38
+ datadog_client: Dogapi::Client,
39
+ # Since `gauge` buckets data points we need to use the same time for all API calls
40
+ # to ensure they fall into the same bucket.
41
+ report_time: Time,
42
+ metrics: T::Array[GaugeMetric]
43
+ ).void
44
+ end
45
+ def self.report!(datadog_client:, report_time:, metrics:)
46
+ #
47
+ # Batching the metrics sends a post request to DD
48
+ # we want to split this up into chunks of 1000 so that as we add more metrics,
49
+ # our payload is not rejected for being too large
50
+ #
51
+ metrics.each_slice(1000).each do |metric_slice|
52
+ datadog_client.batch_metrics do
53
+ metric_slice.each do |metric|
54
+ datadog_client.emit_points(metric.name, [[report_time, metric.count]], type: 'gauge', tags: metric.tags.map(&:to_s))
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,47 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PackStats
5
+ module Private
6
+ module Metrics
7
+ class Files
8
+ extend T::Sig
9
+
10
+ sig do
11
+ params(
12
+ source_code_files: T::Array[SourceCodeFile],
13
+ app_name: String
14
+ ).returns(T::Array[GaugeMetric])
15
+ end
16
+ def self.get_metrics(source_code_files, app_name)
17
+ all_metrics = T.let([], T::Array[GaugeMetric])
18
+ app_level_tag = Tag.for('app', app_name)
19
+
20
+ source_code_files.group_by { |file| file.team_owner&.name }.each do |team_name, files_for_team|
21
+ file_tags = Metrics.tags_for_team(team_name) + [app_level_tag]
22
+ all_metrics += get_file_metrics('by_team', file_tags, files_for_team)
23
+ end
24
+
25
+ file_tags = [app_level_tag]
26
+ all_metrics += get_file_metrics('totals', file_tags, source_code_files)
27
+ all_metrics
28
+ end
29
+
30
+ sig do
31
+ params(
32
+ metric_name_suffix: String,
33
+ tags: T::Array[Tag],
34
+ files: T::Array[SourceCodeFile]
35
+ ).returns(T::Array[GaugeMetric])
36
+ end
37
+ def self.get_file_metrics(metric_name_suffix, tags, files)
38
+ [
39
+ GaugeMetric.for("component_files.#{metric_name_suffix}", files.count(&:componentized_file?), tags),
40
+ GaugeMetric.for("packaged_files.#{metric_name_suffix}", files.count(&:packaged_file?), tags),
41
+ GaugeMetric.for("all_files.#{metric_name_suffix}", files.count, tags),
42
+ ]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,137 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PackStats
5
+ module Private
6
+ module Metrics
7
+ class NestedPacks
8
+ extend T::Sig
9
+
10
+ class PackGroup < T::Struct
11
+ extend T::Sig
12
+
13
+ const :name, String
14
+ const :root, ParsePackwerk::Package
15
+ const :members, T::Array[ParsePackwerk::Package]
16
+
17
+ sig { params(packages: T::Array[ParsePackwerk::Package]).returns(T::Array[PackGroup]) }
18
+ def self.all_from(packages)
19
+ packs_by_group = {}
20
+
21
+ packages.each do |package|
22
+ # For a child pack, package.directory is `packs/fruits/apples` (i.e. the directory of the package.yml file).
23
+ # The package.directory.dirname is therefore `packs/fruits`.
24
+ # For a standalone pack, package.directory.dirname is `packs`
25
+ # A pack with no parent is in a pack group of its own name
26
+ root = ParsePackwerk.find(package.directory.dirname.to_s) || package
27
+ # Mark the parent pack and child pack as being in the pack group of the parent
28
+ packs_by_group[root.name] ||= { root: root, members: [] }
29
+ packs_by_group[root.name][:members] << package
30
+ end
31
+
32
+ packs_by_group.map do |name, pack_data|
33
+ PackGroup.new(
34
+ name: name,
35
+ root: pack_data[:root],
36
+ members: pack_data[:members],
37
+ )
38
+ end
39
+ end
40
+
41
+ sig { returns(Integer) }
42
+ def children_pack_count
43
+ members.count do |package|
44
+ package.name != root.name
45
+ end
46
+ end
47
+
48
+ sig { returns(T::Boolean) }
49
+ def has_parent?
50
+ children_pack_count > 0
51
+ end
52
+
53
+ sig { returns(T::Array[ParsePackwerk::Violation]) }
54
+ def cross_group_violations
55
+ all_violations = members.flat_map do |member|
56
+ ParsePackwerk::DeprecatedReferences.for(member).violations
57
+ end
58
+
59
+ all_violations.select do |violation|
60
+ !members.map(&:name).include?(violation.to_package_name)
61
+ end
62
+ end
63
+ end
64
+
65
+ sig do
66
+ params(
67
+ packages: T::Array[ParsePackwerk::Package],
68
+ app_name: String
69
+ ).returns(T::Array[GaugeMetric])
70
+ end
71
+ def self.get_nested_package_metrics(packages, app_name)
72
+ all_metrics = []
73
+ app_level_tag = Tag.for('app', app_name)
74
+ package_tags = T.let([app_level_tag], T::Array[Tag])
75
+
76
+ pack_groups = PackGroup.all_from(packages)
77
+ all_pack_groups_count = pack_groups.count
78
+ child_pack_count = pack_groups.sum(&:children_pack_count)
79
+ parent_pack_count = pack_groups.count(&:has_parent?)
80
+ all_cross_pack_group_violations = pack_groups.flat_map(&:cross_group_violations)
81
+
82
+ all_metrics << GaugeMetric.for('all_pack_groups.count', all_pack_groups_count, package_tags)
83
+ all_metrics << GaugeMetric.for('child_packs.count', child_pack_count, package_tags)
84
+ all_metrics << GaugeMetric.for('parent_packs.count', parent_pack_count, package_tags)
85
+ all_metrics << GaugeMetric.for('all_pack_groups.privacy_violations.count', Metrics.file_count(all_cross_pack_group_violations.select(&:privacy?)), package_tags)
86
+ all_metrics << GaugeMetric.for('all_pack_groups.dependency_violations.count', Metrics.file_count(all_cross_pack_group_violations.select(&:dependency?)), package_tags)\
87
+
88
+ packs_by_group = {}
89
+ pack_groups.each do |pack_group|
90
+ pack_group.members.each do |member|
91
+ packs_by_group[member.name] = pack_group.name
92
+ end
93
+ end
94
+
95
+ inbound_violations_by_pack_group = {}
96
+ all_cross_pack_group_violations.group_by(&:to_package_name).each do |to_package_name, violations|
97
+ violations.each do |violation|
98
+ pack_group_for_violation = packs_by_group[violation.to_package_name]
99
+ inbound_violations_by_pack_group[pack_group_for_violation] ||= []
100
+ inbound_violations_by_pack_group[pack_group_for_violation] << violation
101
+ end
102
+ end
103
+
104
+ pack_groups.each do |pack_group|
105
+ tags = [
106
+ *package_tags,
107
+ Tag.for('pack_group', Metrics.humanized_package_name(pack_group.name)),
108
+ ]
109
+
110
+ outbound_dependency_violations = pack_group.cross_group_violations.select(&:dependency?)
111
+ inbound_privacy_violations = inbound_violations_by_pack_group.fetch(pack_group.name, []).select(&:privacy?)
112
+ all_metrics << GaugeMetric.for('by_pack_group.outbound_dependency_violations.count', Metrics.file_count(outbound_dependency_violations), tags)
113
+ all_metrics << GaugeMetric.for('by_pack_group.inbound_privacy_violations.count', Metrics.file_count(inbound_privacy_violations), tags)
114
+ end
115
+
116
+ pack_groups.each do |from_pack_group|
117
+ violations_by_to_pack_group = from_pack_group.cross_group_violations.group_by do |violation|
118
+ packs_by_group[violation.to_package_name]
119
+ end
120
+ violations_by_to_pack_group.each do |to_pack_group_name, violations|
121
+ tags = [
122
+ *package_tags,
123
+ Tag.for('pack_group', Metrics.humanized_package_name(from_pack_group.name)),
124
+ Tag.for('to_pack_group', Metrics.humanized_package_name(to_pack_group_name)),
125
+ ]
126
+
127
+ all_metrics << GaugeMetric.for('by_pack_group.outbound_dependency_violations.per_pack_group.count', Metrics.file_count(violations.select(&:dependency?)), tags)
128
+ all_metrics << GaugeMetric.for('by_pack_group.outbound_privacy_violations.per_pack_group.count', Metrics.file_count(violations.select(&:privacy?)), tags)
129
+ end
130
+ end
131
+
132
+ all_metrics
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,105 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PackStats
5
+ module Private
6
+ module Metrics
7
+ class Packages
8
+ extend T::Sig
9
+
10
+ sig do
11
+ params(
12
+ packages: T::Array[ParsePackwerk::Package],
13
+ app_name: String
14
+ ).returns(T::Array[GaugeMetric])
15
+ end
16
+ def self.get_package_metrics(packages, app_name)
17
+ all_metrics = []
18
+ app_level_tag = Tag.for('app', app_name)
19
+ package_tags = T.let([app_level_tag], T::Array[Tag])
20
+
21
+ all_metrics << GaugeMetric.for('all_packages.count', packages.count, package_tags)
22
+ all_metrics << GaugeMetric.for('all_packages.dependencies.count', packages.sum { |package| package.dependencies.count }, package_tags)
23
+ all_metrics << GaugeMetric.for('all_packages.dependency_violations.count', packages.sum { |package| Metrics.file_count(package.violations.select(&:dependency?)) }, package_tags)
24
+ all_metrics << GaugeMetric.for('all_packages.privacy_violations.count', packages.sum { |package| Metrics.file_count(package.violations.select(&:privacy?)) }, package_tags)
25
+ all_metrics << GaugeMetric.for('all_packages.enforcing_dependencies.count', packages.count(&:enforces_dependencies?), package_tags)
26
+ all_metrics << GaugeMetric.for('all_packages.enforcing_privacy.count', packages.count(&:enforces_privacy?), package_tags)
27
+
28
+ all_metrics << GaugeMetric.for('all_packages.with_violations.count', packages.count { |package| package.violations.any? }, package_tags)
29
+ all_metrics += Metrics::PublicUsage.get_public_usage_metrics('all_packages', packages, package_tags)
30
+ all_metrics << GaugeMetric.for('all_packages.has_readme.count', packages.count { |package| Metrics.has_readme?(package) }, package_tags)
31
+
32
+ all_metrics += Metrics::PackwerkCheckerUsage.get_checker_metrics('all_packages', packages, package_tags)
33
+ all_metrics += Metrics::RubocopUsage.get_metrics('all_packages', packages, package_tags)
34
+ all_metrics << GaugeMetric.for('all_packages.package_based_file_ownership.count', packages.count { |package| !package.metadata['owner'].nil? }, package_tags)
35
+
36
+ inbound_violations_by_package = packages.flat_map(&:violations).group_by(&:to_package_name)
37
+
38
+ packages.each do |package|
39
+ package_tags = Metrics.tags_for_package(package, app_name)
40
+
41
+ #
42
+ # VIOLATIONS (implicit dependencies)
43
+ #
44
+ outbound_violations = package.violations
45
+ inbound_violations = inbound_violations_by_package[package.name] || []
46
+ all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?)
47
+ all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?)
48
+
49
+ all_metrics << GaugeMetric.for('by_package.dependency_violations.count', Metrics.file_count(all_dependency_violations), package_tags)
50
+ all_metrics << GaugeMetric.for('by_package.privacy_violations.count', Metrics.file_count(all_privacy_violations), package_tags)
51
+
52
+ all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), package_tags)
53
+ all_metrics << GaugeMetric.for('by_package.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), package_tags)
54
+
55
+ all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), package_tags)
56
+ all_metrics << GaugeMetric.for('by_package.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), package_tags)
57
+
58
+ all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_package', [package], package_tags)
59
+
60
+ package.violations.group_by(&:to_package_name).each do |to_package_name, violations|
61
+ to_package = ParsePackwerk.find(to_package_name)
62
+ if to_package.nil?
63
+ raise StandardError, "Could not find matching package #{to_package_name}"
64
+ end
65
+
66
+ 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)
67
+ all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.per_package.count', Metrics.file_count(violations.select(&:dependency?)), tags)
68
+ all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.per_package.count', Metrics.file_count(violations.select(&:privacy?)), tags)
69
+ end
70
+ end
71
+
72
+ inbound_explicit_dependency_by_package = {}
73
+ packages.each do |package|
74
+ package.dependencies.each do |explicit_dependency|
75
+ inbound_explicit_dependency_by_package[explicit_dependency] ||= []
76
+ inbound_explicit_dependency_by_package[explicit_dependency] << package.name
77
+ end
78
+ end
79
+
80
+ packages.each do |package| # rubocop:disable Style/CombinableLoops
81
+ package_tags = Metrics.tags_for_package(package, app_name)
82
+
83
+ #
84
+ # EXPLICIT DEPENDENCIES
85
+ #
86
+ package.dependencies.each do |explicit_dependency|
87
+ to_package = ParsePackwerk.find(explicit_dependency)
88
+ if to_package.nil?
89
+ raise StandardError, "Could not find matching package #{explicit_dependency}"
90
+ end
91
+
92
+ tags = package_tags + [Tag.for('to_package', Metrics.humanized_package_name(explicit_dependency))] + Metrics.tags_for_to_team(CodeOwnership.for_package(to_package)&.name)
93
+ all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.per_package.count', 1, tags)
94
+ end
95
+
96
+ all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.count', package.dependencies.count, package_tags)
97
+ all_metrics << GaugeMetric.for('by_package.inbound_explicit_dependencies.count', inbound_explicit_dependency_by_package[package.name]&.count || 0, package_tags)
98
+ end
99
+
100
+ all_metrics
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,69 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PackStats
5
+ module Private
6
+ module Metrics
7
+ class PackagesByTeam
8
+ extend T::Sig
9
+
10
+ sig do
11
+ params(
12
+ all_packages: T::Array[ParsePackwerk::Package],
13
+ app_name: String
14
+ ).returns(T::Array[GaugeMetric])
15
+ end
16
+ def self.get_package_metrics_by_team(all_packages, app_name)
17
+ all_metrics = T.let([], T::Array[GaugeMetric])
18
+ app_level_tag = Tag.for('app', app_name)
19
+
20
+ all_packages.group_by { |package| CodeOwnership.for_package(package)&.name }.each do |team_name, packages_by_team|
21
+ # We look at `all_packages` because we care about ALL inbound violations across all teams
22
+ inbound_violations_by_package = all_packages.flat_map(&:violations).group_by(&:to_package_name)
23
+
24
+ team_tags = Metrics.tags_for_team(team_name) + [app_level_tag]
25
+ all_metrics << GaugeMetric.for('by_team.all_packages.count', packages_by_team.count, team_tags)
26
+ all_metrics += Metrics::PackwerkCheckerUsage.get_checker_metrics('by_team', packages_by_team, team_tags)
27
+ all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_team', packages_by_team, team_tags)
28
+ #
29
+ # VIOLATIONS (implicit dependencies)
30
+ #
31
+ outbound_violations = packages_by_team.flat_map(&:violations)
32
+ # Here we only look at packages_by_team because we only care about inbound violations onto packages for this team
33
+ inbound_violations = packages_by_team.flat_map { |package| inbound_violations_by_package[package.name] || [] }
34
+ all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?)
35
+ all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?)
36
+
37
+ all_metrics << GaugeMetric.for('by_team.dependency_violations.count', Metrics.file_count(all_dependency_violations), team_tags)
38
+ all_metrics << GaugeMetric.for('by_team.privacy_violations.count', Metrics.file_count(all_privacy_violations), team_tags)
39
+
40
+ all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), team_tags)
41
+ all_metrics << GaugeMetric.for('by_team.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), team_tags)
42
+
43
+ all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), team_tags)
44
+ all_metrics << GaugeMetric.for('by_team.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), team_tags)
45
+
46
+ all_metrics << GaugeMetric.for('by_team.has_readme.count', packages_by_team.count { |package| Metrics.has_readme?(package) }, team_tags)
47
+
48
+ grouped_outbound_violations = outbound_violations.group_by do |violation|
49
+ to_package = ParsePackwerk.find(violation.to_package_name)
50
+ if to_package.nil?
51
+ raise StandardError, "Could not find matching package #{violation.to_package_name}"
52
+ end
53
+
54
+ CodeOwnership.for_package(to_package)&.name
55
+ end
56
+
57
+ grouped_outbound_violations.each do |to_team_name, violations|
58
+ tags = team_tags + Metrics.tags_for_to_team(to_team_name)
59
+ all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.per_team.count', Metrics.file_count(violations.select(&:dependency?)), tags)
60
+ all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.per_team.count', Metrics.file_count(violations.select(&:privacy?)), tags)
61
+ end
62
+ end
63
+
64
+ all_metrics
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,52 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'rubocop-packs'
5
+
6
+ module PackStats
7
+ module Private
8
+ module Metrics
9
+ class PackwerkCheckerUsage
10
+ extend T::Sig
11
+
12
+ # Later, we might find a way we can get this directly from `packwerk`
13
+ class PackwerkChecker < T::Struct
14
+ const :setting, String
15
+ const :strict_mode, String
16
+ end
17
+
18
+ sig { params(prefix: String, packages: T::Array[ParsePackwerk::Package], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) }
19
+ def self.get_checker_metrics(prefix, packages, package_tags)
20
+ metrics = T.let([], T::Array[GaugeMetric])
21
+
22
+ checkers = [
23
+ PackwerkChecker.new(setting: 'enforce_dependencies', strict_mode: 'enforce_dependencies_strictly'),
24
+ PackwerkChecker.new(setting: 'enforce_privacy', strict_mode: 'enforce_privacy_strictly')
25
+ ]
26
+
27
+ checkers.each do |checker|
28
+ ['false', 'true', 'strict'].each do |enabled_mode|
29
+ count_of_packages = ParsePackwerk.all.count do |package|
30
+ strict_mode = package.metadata[checker.strict_mode]
31
+ enabled = YAML.load_file(package.yml)[checker.setting]
32
+ case enabled_mode
33
+ when 'false'
34
+ !enabled
35
+ when 'true'
36
+ enabled && !strict_mode
37
+ when 'strict'
38
+ !!strict_mode
39
+ end
40
+ end
41
+
42
+ metric_name = "#{prefix}.packwerk_checkers.#{checker.setting}.#{enabled_mode}.count"
43
+ metrics << GaugeMetric.for(metric_name, count_of_packages, package_tags)
44
+ end
45
+ end
46
+
47
+ metrics
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end