Skip to content
Open
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: 2 additions & 0 deletions PluginBuilder/APIModels/InstalledPluginRequest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Newtonsoft.Json;

namespace PluginBuilder.APIModels;

public sealed record InstalledPluginRequest(string Identifier, string Version);
39 changes: 38 additions & 1 deletion PluginBuilder/Controllers/ApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public class ApiController(
UserManager<IdentityUser> userManager,
UserVerifiedLogic userVerifiedLogic,
IHttpClientFactory httpClientFactory,
ServerEnvironment serverEnvironment)
ServerEnvironment serverEnvironment,
TelemetryService telemetryService)
: ControllerBase
{
private sealed class BuildRow
Expand Down Expand Up @@ -200,6 +201,38 @@ ORDER BY lv.ver DESC
return Ok(versions);
}

[AllowAnonymous]
[HttpPost("telemetry/plugins")]
[EnableRateLimiting(Policies.PublicApiRateLimit)]
public async Task<IActionResult> ReportInstalledPlugins([FromBody] List<InstalledPluginRequest> plugins)
{
if (plugins is null || plugins.Count == 0)
return Ok();

var userAgent = Request.Headers.UserAgent.ToString();
var remoteIp = HttpContext.Connection.RemoteIpAddress?.ToString();
var xForwardedFor = Request.Headers["X-Forwarded-For"].ToString();
var xOriginalFor = Request.Headers["X-Original-For"].ToString();

var pluginReports = plugins.Where(p => !string.IsNullOrWhiteSpace(p.Identifier) && !string.IsNullOrWhiteSpace(p.Version))
.Select(p => new PluginReport(p.Identifier, p.Version)).ToList();
Comment on lines +217 to +218
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t map Identifier directly to telemetry Slug.

This stores mixed keys (identifier from snapshot vs slug from download telemetry), fragmenting per-plugin stats and making /plugins/{pluginSlug}/stats inconsistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PluginBuilder/Controllers/ApiController.cs` around lines 217 - 218, The code
is mapping snapshot Identifier into telemetry Slug, which fragments per-plugin
stats; change the projection that builds pluginReports so PluginReport is
constructed with the plugin's canonical slug (e.g., p.Slug or the field used by
download telemetry) instead of p.Identifier — e.g., replace new
PluginReport(p.Identifier, p.Version) with new PluginReport(p.Slug ??
p.Identifier, p.Version) or, preferably, new PluginReport(p.Slug, p.Version) to
ensure telemetry Slug comes from the download/telemetry slug field rather than
the snapshot Identifier.


if (pluginReports.Count == 0)
return Ok();

_ = telemetryService.RecordServerSnapshot(remoteIp, userAgent, pluginReports, xOriginalFor, xForwardedFor);
return Ok();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

[AllowAnonymous]
[HttpGet("plugins/{pluginSlug}/stats")]
[EnableRateLimiting(Policies.PublicApiRateLimit)]
public async Task<IActionResult> GetPluginStats(string pluginSlug)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
var stats = await telemetryService.GetStats(pluginSlug);
return Ok(stats);
}

[AllowAnonymous]
[HttpGet("plugins/{pluginSlug}/versions/{version}")]
[EnableRateLimiting(Policies.PublicApiRateLimit)]
Expand Down Expand Up @@ -249,6 +282,10 @@ public async Task<IActionResult> Download(

await using var conn = await connectionFactory.Open();
await conn.InsertEvent("Download", new JObject { ["pluginSlug"] = pluginSlug.ToString(), ["version"] = version.ToString() });

_ = telemetryService.RecordPluginDownload(pluginSlug.ToString(), version.ToString(), Request.Headers.UserAgent.ToString(),
HttpContext.Connection.RemoteIpAddress?.ToString(), Request.Headers["X-Original-For"].ToString(), Request.Headers["X-Forwarded-For"].ToString());

if (serverEnvironment.EnableLocalArtifactDownloadProxy && Uri.TryCreate(url, UriKind.Absolute, out var artifactUri) && artifactUri.IsLoopback)
{
return RedirectToAction(
Expand Down
16 changes: 16 additions & 0 deletions PluginBuilder/Data/Scripts/22.IncludePluginStatesTable.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS plugin_server_installs
(
hashed_ip TEXT NOT NULL,
plugin_slug TEXT NOT NULL,
version TEXT NOT NULL,
btcpay_version TEXT NOT NULL,
install_count BIGINT NOT NULL DEFAULT 1,
installed_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
uninstalled_at TIMESTAMPTZ NULL,
PRIMARY KEY (hashed_ip, plugin_slug)
);

CREATE INDEX IF NOT EXISTS ix_plugin_server_installs_plugin_slug ON plugin_server_installs (plugin_slug);

CREATE INDEX IF NOT EXISTS ix_plugin_server_installs_plugin_slug_uninstalled ON plugin_server_installs (plugin_slug, uninstalled_at);
23 changes: 23 additions & 0 deletions PluginBuilder/DataModels/PluginDownload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace PluginBuilder.DataModels;

public class PluginDownload
{
public long Id { get; set; }
public string PluginSlug { get; set; } = null!;
public string Version { get; set; } = null!;
public DateTimeOffset Timestamp { get; set; }
public string HashedIp { get; set; } = null!;
public string BTCPayVersion { get; set; } = null!;
}

public class PluginServerInstall
{
public string HashedIp { get; set; } = null!;
public string PluginSlug { get; set; } = null!;
public string Version { get; set; } = null!;
public string BTCPayVersion { get; set; } = null!;
public long InstallCount { get; set; }
public DateTimeOffset InstalledAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public DateTimeOffset? UninstalledAt { get; set; }
}
1 change: 1 addition & 0 deletions PluginBuilder/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
// https://github.com/btcpayserver/btcpayserver-plugin-builder-infra/pull/2
ForwardedHeadersOptions forwardingOptions = new() { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto };
forwardingOptions.KnownNetworks.Clear();
forwardingOptions.KnownProxies.Clear();

Check warning on line 107 in PluginBuilder/Program.cs

View workflow job for this annotation

GitHub Actions / test

'ForwardedHeadersOptions.KnownNetworks' is obsolete: 'Please use KnownIPNetworks instead. For more information, visit https://aka.ms/aspnet/deprecate/005.' (https://aka.ms/aspnet/deprecate/005)
forwardingOptions.ForwardedHeaders = ForwardedHeaders.All;
app.UseForwardedHeaders(forwardingOptions);

Expand Down Expand Up @@ -188,6 +188,7 @@
services.AddHostedService<UserCleanupHostedService>();

services.AddSingleton<DBConnectionFactory>();
services.AddScoped<TelemetryService>();
services.AddScoped<PluginCleanupRunner>();
services.AddScoped<UserCleanupRunner>();
services.AddSingleton<BuildService>();
Expand Down Expand Up @@ -245,11 +246,11 @@
{
context.HttpContext.Response.ContentType = "application/json";
await context.HttpContext.Response.WriteAsJsonAsync(new
{

Check warning on line 249 in PluginBuilder/Program.cs

View workflow job for this annotation

GitHub Actions / test

'ActionContextAccessor' is obsolete: 'ActionContextAccessor is obsolete and will be removed in a future version. For more information, visit https://aka.ms/aspnet/deprecate/006.' (https://aka.ms/aspnet/deprecate/006)

Check warning on line 249 in PluginBuilder/Program.cs

View workflow job for this annotation

GitHub Actions / test

'IActionContextAccessor' is obsolete: 'IActionContextAccessor is obsolete and will be removed in a future version. For more information, visit https://aka.ms/aspnet/deprecate/006.' (https://aka.ms/aspnet/deprecate/006)
code = "429",
message = "Too many requests. Please try again later."
}, cancellationToken);

Check warning on line 252 in PluginBuilder/Program.cs

View workflow job for this annotation

GitHub Actions / test

'IActionContextAccessor' is obsolete: 'IActionContextAccessor is obsolete and will be removed in a future version. For more information, visit https://aka.ms/aspnet/deprecate/006.' (https://aka.ms/aspnet/deprecate/006)
};

Check warning on line 253 in PluginBuilder/Program.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference argument for parameter 'actionContext' in 'UrlHelper.UrlHelper(ActionContext actionContext)'.
options.AddPolicy(Policies.PublicApiRateLimit, httpContext =>
{
var cache = httpContext.RequestServices.GetRequiredService<AdminSettingsCache>();
Expand Down
Loading
Loading