Skip to main content

Package Download Tracking

AvantiPoint Packages automatically tracks detailed information about every package download, providing comprehensive analytics for usage monitoring, security auditing, and business intelligence.

Overview

Every time a package is downloaded, the system records:

  • User identity - The authenticated username (if any)
  • IP address - Remote client IP for geographic and security analysis
  • Client information - NuGet client type and version
  • Platform details - Operating system and version
  • Timestamp - Exact date and time of download
  • User agent - Full HTTP User-Agent string

This data is stored in the PackageDownloads table with a foreign key relationship to the Packages table.

Data Schema

PackageDownload Entity

public class PackageDownload
{
public Guid Id { get; set; } // Unique identifier
public int PackageKey { get; set; } // Reference to package
public IPAddress RemoteIp { get; set; } // Client IP address
public string UserAgentString { get; set; } // Full User-Agent header
public string NuGetClient { get; set; } // Client name (e.g., "NuGet Command Line")
public string NuGetClientVersion { get; set; } // Client version (e.g., "6.8.0")
public string ClientPlatform { get; set; } // OS (e.g., "Windows")
public string ClientPlatformVersion { get; set; }// OS version (e.g., "10.0.22000")
public string User { get; set; } // Authenticated username
public DateTimeOffset Timestamp { get; set; } // Download timestamp (UTC)
}

How It Works

Automatic Tracking

Download tracking happens automatically in the PackageService.AddDownloadAsync method:

public async Task<bool> AddDownloadAsync(string id, NuGetVersion version, CancellationToken cancellationToken)
{
var package = await FindPackageAsync(id, version, cancellationToken);
if (package == null) return false;

// Parse User-Agent for client information
var userAgent = _contextAccessor.HttpContext.Request.Headers["User-Agent"];
var (client, version, platform, platformVersion) = ParseUserAgent(userAgent);

// Get authenticated user
var userName = _contextAccessor.HttpContext.User?.Identity?.Name;

// Record download
_context.PackageDownloads.Add(new PackageDownload
{
PackageKey = package.Key,
RemoteIp = _contextAccessor.HttpContext.Connection.RemoteIpAddress,
ClientPlatform = platform,
ClientPlatformVersion = platformVersion,
NuGetClient = client,
NuGetClientVersion = version,
User = userName,
UserAgentString = userAgent,
});

return await _context.SaveChangesAsync(cancellationToken) > 0;
}

User Agent Parsing

The system intelligently parses User-Agent strings to extract structured information:

Example User-Agent strings:

NuGet Command Line/6.8.0 (Microsoft Windows NT 10.0.22000.0)
→ Client: NuGet Command Line
→ Version: 6.8.0
→ Platform: Windows
→ Platform Version: 10.0.22000.0

dotnet/8.0.0 (win-x64)
→ Client: dotnet
→ Version: 8.0.0
→ Platform: Windows
→ Platform Version: (x64 architecture)

NuGet xplat/6.4.0 (Linux 5.15.0)
→ Client: NuGet xplat
→ Version: 6.4.0
→ Platform: Linux
→ Platform Version: 5.15.0

Use Cases

1. Usage Analytics

Query download patterns to understand package adoption:

-- Most downloaded packages in the last 30 days
SELECT
p.Id,
p.Version,
COUNT(*) as Downloads
FROM PackageDownloads pd
INNER JOIN Packages p ON pd.PackageKey = p.[Key]
WHERE pd.Timestamp >= DATEADD(day, -30, GETUTCDATE())
GROUP BY p.Id, p.Version
ORDER BY Downloads DESC;

2. Security Auditing

Track downloads by IP address for security monitoring:

-- Detect unusual download patterns from single IP
SELECT
RemoteIp,
COUNT(DISTINCT PackageKey) as UniquePackages,
COUNT(*) as TotalDownloads
FROM PackageDownloads
WHERE Timestamp >= DATEADD(hour, -1, GETUTCDATE())
GROUP BY RemoteIp
HAVING COUNT(*) > 100
ORDER BY TotalDownloads DESC;

3. License Compliance

For commercial packages, track which users are downloading:

-- Downloads by user for licensing verification
SELECT
pd.User,
p.Id as PackageId,
COUNT(*) as DownloadCount,
MAX(pd.Timestamp) as LastDownload
FROM PackageDownloads pd
INNER JOIN Packages p ON pd.PackageKey = p.[Key]
WHERE p.Id LIKE 'MyCompany.Premium%'
AND pd.User IS NOT NULL
GROUP BY pd.User, p.Id
ORDER BY pd.User, DownloadCount DESC;

4. Client Version Analysis

Understand what tooling your users are running:

-- NuGet client version distribution
SELECT
NuGetClient,
NuGetClientVersion,
COUNT(*) as UsageCount
FROM PackageDownloads
WHERE Timestamp >= DATEADD(day, -7, GETUTCDATE())
AND NuGetClient IS NOT NULL
GROUP BY NuGetClient, NuGetClientVersion
ORDER BY UsageCount DESC;

5. Platform Insights

Identify which platforms are consuming your packages:

-- Operating system distribution
SELECT
ClientPlatform,
ClientPlatformVersion,
COUNT(*) as Downloads
FROM PackageDownloads
WHERE Timestamp >= DATEADD(month, -1, GETUTCDATE())
AND ClientPlatform IS NOT NULL
GROUP BY ClientPlatform, ClientPlatformVersion
ORDER BY Downloads DESC;

Integration with Callbacks

Download tracking works seamlessly with event callbacks. Use INuGetFeedActionHandler to react to downloads:

public class DownloadAnalyticsHandler : INuGetFeedActionHandler
{
private readonly ILogger<DownloadAnalyticsHandler> _logger;
private readonly IHttpContextAccessor _contextAccessor;

public async Task OnPackageDownloaded(string packageId, string version)
{
var context = _contextAccessor.HttpContext;
var ip = context.Connection.RemoteIpAddress;
var user = context.User.Identity?.Name;
var userAgent = context.Request.Headers["User-Agent"].ToString();

_logger.LogInformation(
"Package {PackageId} {Version} downloaded by {User} from {IP} using {UserAgent}",
packageId, version, user ?? "anonymous", ip, userAgent);

// Trigger custom business logic
await SendToAnalyticsService(packageId, version, user, ip);
await CheckLicenseCompliance(packageId, user);
await UpdateUsageMetrics(packageId);
}

// Other handler methods...
}

Database Views for Performance

AvantiPoint Packages includes pre-built views for efficient download analytics:

vw_PackageDownloadCounts

Pre-aggregated download counts per package:

CREATE VIEW vw_PackageDownloadCounts AS
SELECT
p.[Key] as PackageKey,
p.Id as PackageId,
p.Version,
COUNT(pd.Id) as DownloadCount
FROM Packages p
LEFT JOIN PackageDownloads pd ON p.[Key] = pd.PackageKey
GROUP BY p.[Key], p.Id, p.Version;

Usage:

var topPackages = await context.Database
.SqlQuery<PackageDownloadSummary>($@"
SELECT TOP 10 PackageId, SUM(DownloadCount) as TotalDownloads
FROM vw_PackageDownloadCounts
GROUP BY PackageId
ORDER BY TotalDownloads DESC")
.ToListAsync();

Privacy Considerations

IP Address Storage

IP addresses are stored as IPAddress objects and can identify individual users. Consider:

  • Data retention policies - Automatically purge old download records
  • Anonymization - Hash or truncate IP addresses for privacy
  • Regional compliance - Ensure GDPR/CCPA compliance if applicable

User Information

The User field contains authenticated usernames. Ensure:

  • Users are informed about download tracking in your terms of service
  • Access to download data is restricted to authorized administrators
  • Personal data is handled according to your privacy policy

Configuration

Disabling Download Tracking

Download tracking cannot be fully disabled, but you can:

  1. Limit data retention - Periodically clean old records:
-- Delete download records older than 90 days
DELETE FROM PackageDownloads
WHERE Timestamp < DATEADD(day, -90, GETUTCDATE());
  1. Anonymize in real-time - Override AddDownloadAsync in a custom service:
public class AnonymizedPackageService : PackageService
{
protected override async Task<bool> AddDownloadAsync(...)
{
// Store download without IP/User
_context.PackageDownloads.Add(new PackageDownload
{
PackageKey = package.Key,
RemoteIp = null, // Omit IP
User = null, // Omit user
// ... other fields
});

return await _context.SaveChangesAsync(cancellationToken) > 0;
}
}

Export and Reporting

Export to CSV

public async Task<string> ExportDownloadsToCsv(DateTime startDate, DateTime endDate)
{
var downloads = await _context.PackageDownloads
.Include(d => d.Package)
.Where(d => d.Timestamp >= startDate && d.Timestamp <= endDate)
.OrderByDescending(d => d.Timestamp)
.ToListAsync();

var csv = new StringBuilder();
csv.AppendLine("Timestamp,Package,Version,User,IP,Client,Platform");

foreach (var download in downloads)
{
csv.AppendLine($"{download.Timestamp:O},{download.Package.Id}," +
$"{download.Package.Version},{download.User ?? "anonymous"}," +
$"{download.RemoteIp},{download.NuGetClient}," +
$"{download.ClientPlatform}");
}

return csv.ToString();
}

Power BI Integration

Connect Power BI directly to your database for real-time dashboards:

  1. Create a SQL view with the needed aggregations
  2. Use DirectQuery or Import mode in Power BI
  3. Build visualizations for:
    • Downloads over time
    • Top packages by downloads
    • Geographic distribution (if IP geolocation added)
    • Client version adoption

Best Practices

  1. Index PackageKey - Already indexed; critical for query performance
  2. Partition by date - For large feeds, consider table partitioning on Timestamp
  3. Archive old data - Move records older than 1 year to archive tables
  4. Monitor table growth - Download tables can grow quickly on busy feeds
  5. Use views - Leverage pre-built views instead of raw queries
  6. Batch deletes - When purging old data, delete in batches to avoid locking

Troubleshooting

Download counts seem low

  • Verify that AddDownloadAsync is being called
  • Check for errors in application logs
  • Ensure database migrations have run successfully
  • Verify PackageDownloads table exists

Missing user information

  • User will be null for anonymous downloads
  • Check that authentication is properly configured
  • Verify IHttpContextAccessor is registered in DI

IP address shows as null

  • May occur behind certain proxies/load balancers
  • Configure proxy headers: app.UseForwardedHeaders()
  • Check RemoteIpAddress configuration

See Also