Skip to content

Commit d5c54fb

Browse files
committed
checking in changes for ios and android add ons
1 parent 410533b commit d5c54fb

28 files changed

Lines changed: 1038 additions & 19 deletions

app/assets/stylesheets/application.tailwind.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,15 @@
472472
.project-type-icon-dotnet {
473473
@apply bg-violet-50 text-violet-800 ring-1 ring-violet-200;
474474
}
475+
.project-type-icon-cloudflare_pages {
476+
@apply bg-orange-50 text-orange-700 ring-1 ring-orange-200;
477+
}
478+
.project-type-icon-android {
479+
@apply bg-lime-50 text-lime-800 ring-1 ring-lime-200;
480+
}
481+
.project-type-icon-ios {
482+
@apply bg-zinc-50 text-zinc-800 ring-1 ring-zinc-200;
483+
}
475484
.project-type-icon-http_api {
476485
@apply bg-emerald-50 text-emerald-800 ring-1 ring-emerald-200;
477486
}

app/controllers/concerns/project_settings_context.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ def load_project_settings_context
1313
.select(:id, :uuid, :project_id, :name, :last_used_at, :revoked_at, :created_at)
1414
.order(created_at: :desc)
1515
@notification_preference ||= ProjectNotificationPreference.for(user: current_user, project: @project)
16+
@cloudflare_integration_setting ||= ProjectIntegrationSetting.for(
17+
project: @project,
18+
provider: ProjectIntegrationSetting::PROVIDERS[:cloudflare_pages]
19+
) if @project.integration_cloudflare_pages?
1620
@assignment_summary = ProjectAssignmentSummary.new(@project)
1721
@retention_policy ||= ProjectRetentionPolicy.for(project: @project) if @project.owned_by?(current_user)
1822
@public_api_rate_limit_defaults = {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
class ProjectIntegrationSettingsController < ApplicationController
2+
include ProjectScope
3+
include ProjectSettingsContext
4+
5+
before_action :authenticate_user!
6+
before_action :set_owned_project
7+
8+
def update
9+
@integration_setting = ProjectIntegrationSetting.for(
10+
project: @project,
11+
provider: integration_setting_params.fetch(:provider)
12+
)
13+
@integration_setting.assign_attributes(integration_setting_params)
14+
15+
if @integration_setting.save
16+
redirect_to settings_project_path(@project, anchor: "platform-integration"),
17+
notice: "#{@integration_setting.provider.humanize} settings updated."
18+
else
19+
@cloudflare_integration_setting = @integration_setting if @integration_setting.provider_cloudflare_pages?
20+
load_project_settings_context
21+
render "projects/settings", status: :unprocessable_content
22+
end
23+
end
24+
25+
private
26+
27+
def integration_setting_params
28+
params.require(:project_integration_setting).permit(
29+
:provider,
30+
:enabled,
31+
:account_id,
32+
:external_project_id,
33+
:external_project_name,
34+
:credential_reference
35+
).tap do |permitted|
36+
permitted[:enabled] = ActiveModel::Type::Boolean.new.cast(permitted[:enabled]) if permitted.key?(:enabled)
37+
end
38+
end
39+
end

app/controllers/projects_controller.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def new
6262
end
6363

6464
def create
65-
@project = current_user.projects.new(project_params)
65+
@project = current_user.projects.new(project_create_params)
6666
if @project.save
6767
redirect_to settings_project_path(@project, anchor: "integration-guide"),
6868
notice: "Project created. Follow the #{@project.integration_label} setup guide to start ingesting events."
@@ -75,7 +75,7 @@ def edit
7575
end
7676

7777
def update
78-
if @project.update(project_params)
78+
if @project.update(project_update_params)
7979
redirect_to settings_project_path(@project), notice: "Project updated."
8080
else
8181
render :edit, status: :unprocessable_content
@@ -165,10 +165,14 @@ def legacy_inbox_request?
165165
(request.query_parameters.keys & LEGACY_INBOX_PARAMS).any?
166166
end
167167

168-
def project_params
168+
def project_create_params
169169
params.require(:project).permit(:name, :description, :integration_kind)
170170
end
171171

172+
def project_update_params
173+
params.require(:project).permit(:name, :description)
174+
end
175+
172176
def filtered_projects(scope, filter)
173177
case filter
174178
when "archived" then scope.archived

app/helpers/application_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ module ApplicationHelper
3535
"javascript" => :project_javascript,
3636
"python" => :project_python,
3737
"dotnet" => :project_dotnet,
38+
"cloudflare_pages" => :external,
39+
"android" => :projects,
40+
"ios" => :projects,
3841
"http_api" => :external
3942
}.freeze
4043

app/helpers/projects_helper.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ module ProjectsHelper
1010
badge: "NuGet",
1111
description: "ASP.NET Core apps, .NET workers, C# services, and custom metrics with logister-dotnet."
1212
},
13+
"cloudflare_pages" => {
14+
label: "Cloudflare Pages",
15+
badge: "Importer",
16+
description: "Pages deployments, Web Analytics, traffic rollups, build health, and domain-level site signals."
17+
},
18+
"android" => {
19+
label: "Android app",
20+
badge: "Gradle",
21+
description: "Android app telemetry from logister-android, plus optional Google Play vitals imports."
22+
},
23+
"ios" => {
24+
label: "iOS app",
25+
badge: "SPM",
26+
description: "iOS app telemetry from logister-ios, plus optional App Store Connect analytics imports."
27+
},
1328
"cfml" => {
1429
label: "CFML",
1530
badge: "CFML",
@@ -44,6 +59,9 @@ def project_integration_docs_path(project)
4459
return docs_site_url(:javascript_integration) if project&.integration_javascript?
4560
return docs_site_url(:python_integration) if project&.integration_python?
4661
return docs_site_url(:http_api) if project&.integration_http_api?
62+
return docs_site_url(:http_api) if project&.integration_cloudflare_pages?
63+
return docs_site_url(:http_api) if project&.integration_android?
64+
return docs_site_url(:http_api) if project&.integration_ios?
4765

4866
docs_site_url(:ruby_integration)
4967
end
@@ -54,6 +72,9 @@ def project_integration_docs_label(project)
5472
return "JavaScript integration docs" if project&.integration_javascript?
5573
return "Python integration docs" if project&.integration_python?
5674
return "HTTP API docs" if project&.integration_http_api?
75+
return "HTTP API docs" if project&.integration_cloudflare_pages?
76+
return "HTTP API docs" if project&.integration_android?
77+
return "HTTP API docs" if project&.integration_ios?
5778

5879
"Ruby integration docs"
5980
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class CloudflarePagesImportJob < ApplicationJob
2+
queue_as :default
3+
4+
def perform(project_integration_setting_id)
5+
setting = ProjectIntegrationSetting.find_by(id: project_integration_setting_id)
6+
Logister::CloudflarePagesImporter.call(setting)
7+
end
8+
end

app/models/project.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class Project < ApplicationRecord
1515
has_many :check_in_monitors, dependent: :destroy
1616
has_many :project_memberships, dependent: :destroy
1717
has_many :project_notification_preferences, dependent: :destroy
18+
has_many :integration_settings, class_name: "ProjectIntegrationSetting", dependent: :destroy
1819
has_many :email_notification_deliveries, dependent: :destroy
1920
has_one :retention_policy, class_name: "ProjectRetentionPolicy", dependent: :destroy
2021
has_many :telemetry_archives, dependent: :destroy
@@ -23,12 +24,17 @@ class Project < ApplicationRecord
2324
before_validation :ensure_uuid
2425
before_validation :normalize_slug
2526

27+
validate :integration_kind_cannot_change, on: :update
28+
2629
enum :integration_kind, {
2730
ruby: "ruby",
2831
cfml: "cfml",
2932
javascript: "javascript",
3033
python: "python",
3134
dotnet: "dotnet",
35+
cloudflare_pages: "cloudflare_pages",
36+
android: "android",
37+
ios: "ios",
3238
http_api: "http_api"
3339
}, default: :ruby, validate: true, prefix: :integration
3440

@@ -111,6 +117,9 @@ def integration_label
111117
"javascript" => "JavaScript / TypeScript",
112118
"python" => "Python",
113119
"dotnet" => ".NET / ASP.NET Core",
120+
"cloudflare_pages" => "Cloudflare Pages",
121+
"android" => "Android app",
122+
"ios" => "iOS app",
114123
"http_api" => "Manual / HTTP API"
115124
}.fetch(integration_kind, integration_kind.to_s.humanize)
116125
end
@@ -130,6 +139,9 @@ def public_api_auth_failure_rate_limit_requests_effective(default)
130139
def self.integration_options
131140
[
132141
[ "Manual / HTTP API (custom client)", "http_api" ],
142+
[ "Cloudflare Pages", "cloudflare_pages" ],
143+
[ "Android app (logister-android)", "android" ],
144+
[ "iOS app (logister-ios)", "ios" ],
133145
[ "Ruby gem", "ruby" ],
134146
[ ".NET / ASP.NET Core (logister-dotnet)", "dotnet" ],
135147
[ "JavaScript / TypeScript (logister-js)", "javascript" ],
@@ -241,4 +253,10 @@ def normalize_slug
241253
base = slug.presence || name
242254
self.slug = base.to_s.parameterize
243255
end
256+
257+
def integration_kind_cannot_change
258+
return unless will_save_change_to_integration_kind?
259+
260+
errors.add(:integration_kind, "cannot be changed after project creation")
261+
end
244262
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
class ProjectIntegrationSetting < ApplicationRecord
2+
PROVIDERS = {
3+
cloudflare_pages: "cloudflare_pages",
4+
google_play: "google_play",
5+
app_store_connect: "app_store_connect"
6+
}.freeze
7+
8+
belongs_to :project
9+
10+
before_validation :ensure_uuid
11+
before_validation :normalize_fields
12+
13+
enum :provider, PROVIDERS, validate: true, prefix: true
14+
15+
validates :uuid, presence: true, uniqueness: true
16+
validates :provider, presence: true, uniqueness: { scope: :project_id }
17+
validates :account_id, presence: true, if: :provider_cloudflare_pages?
18+
validates :external_project_name, presence: true, if: :provider_cloudflare_pages?
19+
validate :provider_matches_project_integration
20+
21+
scope :enabled, -> { where(enabled: true) }
22+
scope :cloudflare_pages, -> { where(provider: PROVIDERS[:cloudflare_pages]) }
23+
scope :due_for_import, ->(before: 15.minutes.ago) {
24+
enabled.where("last_imported_at IS NULL OR last_imported_at <= ?", before)
25+
}
26+
27+
def to_param
28+
uuid
29+
end
30+
31+
def self.for(project:, provider:)
32+
find_or_initialize_by(project: project, provider: provider.to_s)
33+
end
34+
35+
def configured?
36+
case provider
37+
when PROVIDERS[:cloudflare_pages]
38+
enabled? && account_id.present? && external_project_name.present? && credential_reference.present?
39+
else
40+
enabled?
41+
end
42+
end
43+
44+
private
45+
46+
def ensure_uuid
47+
self.uuid ||= SecureRandom.uuid
48+
end
49+
50+
def normalize_fields
51+
self.account_id = account_id.to_s.strip.presence
52+
self.external_project_id = external_project_id.to_s.strip.presence
53+
self.external_project_name = external_project_name.to_s.strip.presence
54+
self.credential_reference = credential_reference.to_s.strip.presence
55+
self.metadata = metadata.is_a?(Hash) ? metadata : {}
56+
end
57+
58+
def provider_matches_project_integration
59+
return if project.blank? || provider.blank?
60+
return if provider_cloudflare_pages? && project.integration_cloudflare_pages?
61+
return if provider_google_play? && project.integration_android?
62+
return if provider_app_store_connect? && project.integration_ios?
63+
64+
errors.add(:provider, "does not match this project's integration type")
65+
end
66+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module Logister
2+
class CloudflarePagesImporter
3+
Result = Data.define(:status, :reason, :setting) do
4+
def imported? = status == :imported
5+
def skipped? = status == :skipped
6+
end
7+
8+
def self.call(setting)
9+
new(setting).call
10+
end
11+
12+
def initialize(setting)
13+
@setting = setting
14+
end
15+
16+
def call
17+
return Result.new(status: :skipped, reason: :missing_setting, setting: nil) unless setting
18+
return Result.new(status: :skipped, reason: :wrong_provider, setting: setting) unless setting.provider_cloudflare_pages?
19+
return Result.new(status: :skipped, reason: :not_configured, setting: setting) unless setting.configured?
20+
21+
# The API fetcher lands here next. Keeping the job/service boundary now
22+
# lets settings, scheduling, and result handling settle before credentials
23+
# or Cloudflare API calls enter the code path.
24+
Result.new(status: :skipped, reason: :fetcher_not_implemented, setting: setting)
25+
end
26+
27+
private
28+
29+
attr_reader :setting
30+
end
31+
end

0 commit comments

Comments
 (0)