From 3aea3d6640546b6aefaacea625201dc9a9eed9a5 Mon Sep 17 00:00:00 2001 From: scbedd <45376673+scbedd@users.noreply.github.com> Date: Thu, 28 Feb 2019 17:08:07 -0800 Subject: [PATCH 1/2] initial commit pending feedback --- web/pixel-server/PixelServer.sln | 25 +++++ .../ConnectedService.json | 7 ++ .../Controllers/TrackingController.cs | 96 ++++++++++++++++++ web/pixel-server/PixelServer/Etc/pixel.png | Bin 0 -> 119 bytes .../PixelServer/PixelServer.csproj | 30 ++++++ web/pixel-server/PixelServer/Program.cs | 25 +++++ .../Properties/launchSettings.json | 30 ++++++ web/pixel-server/PixelServer/Startup.cs | 48 +++++++++ .../PixelServer/appsettings.Development.json | 9 ++ web/pixel-server/PixelServer/appsettings.json | 11 ++ 10 files changed, 281 insertions(+) create mode 100644 web/pixel-server/PixelServer.sln create mode 100644 web/pixel-server/PixelServer/Connected Services/Application Insights/ConnectedService.json create mode 100644 web/pixel-server/PixelServer/Controllers/TrackingController.cs create mode 100644 web/pixel-server/PixelServer/Etc/pixel.png create mode 100644 web/pixel-server/PixelServer/PixelServer.csproj create mode 100644 web/pixel-server/PixelServer/Program.cs create mode 100644 web/pixel-server/PixelServer/Properties/launchSettings.json create mode 100644 web/pixel-server/PixelServer/Startup.cs create mode 100644 web/pixel-server/PixelServer/appsettings.Development.json create mode 100644 web/pixel-server/PixelServer/appsettings.json diff --git a/web/pixel-server/PixelServer.sln b/web/pixel-server/PixelServer.sln new file mode 100644 index 00000000000..5c064d6887c --- /dev/null +++ b/web/pixel-server/PixelServer.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.106 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixelServer", "PixelServer\PixelServer.csproj", "{65040EF3-BB67-48A7-8357-908D84075F78}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {65040EF3-BB67-48A7-8357-908D84075F78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65040EF3-BB67-48A7-8357-908D84075F78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65040EF3-BB67-48A7-8357-908D84075F78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65040EF3-BB67-48A7-8357-908D84075F78}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {97DEB234-007A-48F6-AFB4-9542BE0EF08C} + EndGlobalSection +EndGlobal diff --git a/web/pixel-server/PixelServer/Connected Services/Application Insights/ConnectedService.json b/web/pixel-server/PixelServer/Connected Services/Application Insights/ConnectedService.json new file mode 100644 index 00000000000..94232b3815c --- /dev/null +++ b/web/pixel-server/PixelServer/Connected Services/Application Insights/ConnectedService.json @@ -0,0 +1,7 @@ +{ + "ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider", + "Version": "8.14.11009.1", + "GettingStartedDocument": { + "Uri": "https://go.microsoft.com/fwlink/?LinkID=798432" + } +} \ No newline at end of file diff --git a/web/pixel-server/PixelServer/Controllers/TrackingController.cs b/web/pixel-server/PixelServer/Controllers/TrackingController.cs new file mode 100644 index 00000000000..641fbd7d038 --- /dev/null +++ b/web/pixel-server/PixelServer/Controllers/TrackingController.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ApplicationInsights; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Primitives; + +namespace PixelServer.Controllers +{ + [Route("api/impressions")] + [ApiController] + public class TrackingController : ControllerBase + { + // used for manual logs to application insights + private static TelemetryClient telemetry; + + // populated and returned as 1 pixel image + private static byte[] img; + private static string imgPath = AppDomain.CurrentDomain.BaseDirectory + "/Etc/pixel.png"; + + private IMemoryCache _cache; + + public TrackingController(IMemoryCache memoryCache) + { + _cache = memoryCache; + } + + /// + /// Currently the only entrypoint, services a 1 pixel image while recording that there was an impression for the specific path. + /// + /// Uses in-memory cache of afore-seen IPs to recognize a "duplicate" impression. The addresses themselves are not recorded. + /// + /// + /// + [HttpGet] + public async Task Get(string path) + { + await trackEventAsync(path); + + return File(getCachedImage(), "image/png"); + } + + private async Task trackEventAsync(string path) + { + if (telemetry == null) + { + telemetry = new TelemetryClient(); + } + + await Task.Run(() => { + telemetry.TrackEvent("PixelImpression", new Dictionary() + { + { "visitor_duplicate", getCachedVisitorStatus(getRequestAddress()) }, + { "visitor_path", path } + }); + }); + } + + private string getCachedVisitorStatus(string address) + { + if (!_cache.TryGetValue(address, out _)) + { + // Set cache options. + var cacheEntryOptions = new MemoryCacheEntryOptions() + // Keep in cache for this time, reset time if accessed. + .SetSlidingExpiration(TimeSpan.FromHours(1)); + + // Save data in cache. + _cache.Set(address, DateTime.UtcNow, cacheEntryOptions); + + return "false"; + } + + return "true"; + } + + private byte[] getCachedImage() + { + if (img == null) + { + img = System.IO.File.ReadAllBytes(imgPath); + } + + return img; + } + + private string getRequestAddress() + { + return Request.HttpContext.Connection.RemoteIpAddress.ToString(); + } + } +} diff --git a/web/pixel-server/PixelServer/Etc/pixel.png b/web/pixel-server/PixelServer/Etc/pixel.png new file mode 100644 index 0000000000000000000000000000000000000000..818c71d03f435db011069584cda25c1f66af1a85 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2s6ii6yp7}lMWc?smOq&xaLGB9lH z=l+w(3gmMZctjR6Fz_7)VaDV6D^h@hJf1F&Arj%qKmPx>XJGxu^l!OoV+&B6!PC{x JWt~$(69DNq9##MV literal 0 HcmV?d00001 diff --git a/web/pixel-server/PixelServer/PixelServer.csproj b/web/pixel-server/PixelServer/PixelServer.csproj new file mode 100644 index 00000000000..6d51201f4a7 --- /dev/null +++ b/web/pixel-server/PixelServer/PixelServer.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp2.1 + /subscriptions/a18897a6-7e44-457d-9260-f2854c0aca42/resourcegroups/sdk-bi-data/providers/microsoft.insights/components/azure-sdk-bi + /subscriptions/a18897a6-7e44-457d-9260-f2854c0aca42/resourcegroups/sdk-bi-data/providers/microsoft.insights/components/azure-sdk-bi + 317bfab0-0fc1-4048-b61e-67e8f96c2b3a + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/web/pixel-server/PixelServer/Program.cs b/web/pixel-server/PixelServer/Program.cs new file mode 100644 index 00000000000..2b3d536c913 --- /dev/null +++ b/web/pixel-server/PixelServer/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace PixelServer +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseApplicationInsights() + .UseStartup(); + } +} diff --git a/web/pixel-server/PixelServer/Properties/launchSettings.json b/web/pixel-server/PixelServer/Properties/launchSettings.json new file mode 100644 index 00000000000..f0cc6fcf160 --- /dev/null +++ b/web/pixel-server/PixelServer/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:64742", + "sslPort": 44395 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "PixelServer": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/web/pixel-server/PixelServer/Startup.cs b/web/pixel-server/PixelServer/Startup.cs new file mode 100644 index 00000000000..aa21439be3c --- /dev/null +++ b/web/pixel-server/PixelServer/Startup.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace PixelServer +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMemoryCache(); + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseMvc(); + } + } +} diff --git a/web/pixel-server/PixelServer/appsettings.Development.json b/web/pixel-server/PixelServer/appsettings.Development.json new file mode 100644 index 00000000000..e203e9407e7 --- /dev/null +++ b/web/pixel-server/PixelServer/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/web/pixel-server/PixelServer/appsettings.json b/web/pixel-server/PixelServer/appsettings.json new file mode 100644 index 00000000000..f5127177441 --- /dev/null +++ b/web/pixel-server/PixelServer/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*", + "ApplicationInsights": { + "InstrumentationKey": "SetInAzure" + } +} \ No newline at end of file From 2262e0b0c49912a6c38cd7d5918f644c843d021f Mon Sep 17 00:00:00 2001 From: scbedd <45376673+scbedd@users.noreply.github.com> Date: Thu, 28 Feb 2019 17:08:43 -0800 Subject: [PATCH 2/2] updating gitignore. adding pixel server. removing extraneous dependencies. --- .gitignore | 346 ++++++++++++++++++ README.md | 1 + .../Controllers/TrackingController.cs | 63 ++-- web/pixel-server/PixelServer/Program.cs | 16 +- .../Properties/launchSettings.json | 6 +- web/pixel-server/PixelServer/Startup.cs | 3 +- web/pixel-server/PixelServer/appsettings.json | 2 +- web/pixel-server/README.md | 52 +++ web/pixel-server/example.png | Bin 0 -> 34849 bytes 9 files changed, 440 insertions(+), 49 deletions(-) create mode 100644 web/pixel-server/README.md create mode 100644 web/pixel-server/example.png diff --git a/.gitignore b/.gitignore index 894a44cc066..6c3c2bc51f5 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,349 @@ venv.bak/ # mypy .mypy_cache/ + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ +# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true +**/wwwroot/lib/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ \ No newline at end of file diff --git a/README.md b/README.md index 1e20d1fdfa5..231bbb3c144 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This repository contains useful tools that the Azure SDK team utilizes across th | Package or Intent | Path | Description | Status | |-------------------|-----------------------------------------|-----------------------------------------------------------------|----------| | doc-warden | [Readme](packages/python-packages/doc-warden/README.md) | A tool used to enforce readme standards across Azure SDK Repos. |[![Build Status](https://dev.azure.com/azure-sdk/public/_apis/build/status/108?branchName=master)](https://dev.azure.com/azure-sdk/public/_build/latest?definitionId=108&branchName=master) | +| pixel-server | [Readme](packages/web/pixel-server/README.md) | A tiny ASP.NET Core site used to serve a pixel and record impressions. | Not Yet Enabled | # Contributing diff --git a/web/pixel-server/PixelServer/Controllers/TrackingController.cs b/web/pixel-server/PixelServer/Controllers/TrackingController.cs index 641fbd7d038..30946eaec47 100644 --- a/web/pixel-server/PixelServer/Controllers/TrackingController.cs +++ b/web/pixel-server/PixelServer/Controllers/TrackingController.cs @@ -5,9 +5,9 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.ApplicationInsights; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Primitives; namespace PixelServer.Controllers { @@ -15,18 +15,13 @@ namespace PixelServer.Controllers [ApiController] public class TrackingController : ControllerBase { - // used for manual logs to application insights - private static TelemetryClient telemetry; + private readonly IMemoryCache _cache; + private readonly TelemetryClient _telemetry; - // populated and returned as 1 pixel image - private static byte[] img; - private static string imgPath = AppDomain.CurrentDomain.BaseDirectory + "/Etc/pixel.png"; - - private IMemoryCache _cache; - - public TrackingController(IMemoryCache memoryCache) + public TrackingController(IMemoryCache memoryCache, TelemetryClient telemetry) { _cache = memoryCache; + _telemetry = telemetry; } /// @@ -37,30 +32,18 @@ public TrackingController(IMemoryCache memoryCache) /// /// [HttpGet] - public async Task Get(string path) - { - await trackEventAsync(path); - - return File(getCachedImage(), "image/png"); - } - - private async Task trackEventAsync(string path) + public IActionResult Get(string path) { - if (telemetry == null) + _telemetry.TrackEvent("PixelImpression", new Dictionary() { - telemetry = new TelemetryClient(); - } - - await Task.Run(() => { - telemetry.TrackEvent("PixelImpression", new Dictionary() - { - { "visitor_duplicate", getCachedVisitorStatus(getRequestAddress()) }, - { "visitor_path", path } - }); + { "visitor_duplicate", getCachedVisitorStatus(getRequestAddress()) }, + { "visitor_path", path } }); + + return new ImageActionResult(); } - private string getCachedVisitorStatus(string address) + private string getCachedVisitorStatus(System.Net.IPAddress address) { if (!_cache.TryGetValue(address, out _)) { @@ -78,19 +61,23 @@ private string getCachedVisitorStatus(string address) return "true"; } - private byte[] getCachedImage() + private System.Net.IPAddress getRequestAddress() { - if (img == null) - { - img = System.IO.File.ReadAllBytes(imgPath); - } - - return img; + return Request.HttpContext.Connection.RemoteIpAddress; } - private string getRequestAddress() + private class ImageActionResult : IActionResult { - return Request.HttpContext.Connection.RemoteIpAddress.ToString(); + private static readonly byte[] _imgPayload = System.IO.File.ReadAllBytes(AppDomain.CurrentDomain.BaseDirectory + "/Etc/pixel.png"); + + public Task ExecuteResultAsync(ActionContext context) + { + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.HttpContext.Response.ContentType = "image/png"; + context.HttpContext.Response.ContentLength = _imgPayload.Length; + return context.HttpContext.Response.Body.WriteAsync(_imgPayload, 0, _imgPayload.Length); + } } + } } diff --git a/web/pixel-server/PixelServer/Program.cs b/web/pixel-server/PixelServer/Program.cs index 2b3d536c913..c9cd1c52136 100644 --- a/web/pixel-server/PixelServer/Program.cs +++ b/web/pixel-server/PixelServer/Program.cs @@ -14,12 +14,16 @@ public class Program { public static void Main(string[] args) { - CreateWebHostBuilder(args).Build().Run(); - } + var webHostBuilder = new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .UseDefaultServiceProvider( + (context, options) => options.ValidateScopes = context.HostingEnvironment.IsDevelopment()) + .UseKestrel(); - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseApplicationInsights() - .UseStartup(); + webHostBuilder.UseSockets(x => x.IOQueueCount = 2); + webHostBuilder.UseApplicationInsights(); + webHostBuilder.Build().Run(); + } } } diff --git a/web/pixel-server/PixelServer/Properties/launchSettings.json b/web/pixel-server/PixelServer/Properties/launchSettings.json index f0cc6fcf160..ff695889bcd 100644 --- a/web/pixel-server/PixelServer/Properties/launchSettings.json +++ b/web/pixel-server/PixelServer/Properties/launchSettings.json @@ -12,7 +12,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "api/values", + "launchUrl": "api/impressions", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -20,11 +20,11 @@ "PixelServer": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "api/values", + "launchUrl": "api/impressions", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} \ No newline at end of file +} diff --git a/web/pixel-server/PixelServer/Startup.cs b/web/pixel-server/PixelServer/Startup.cs index aa21439be3c..bcd1ba4bc6d 100644 --- a/web/pixel-server/PixelServer/Startup.cs +++ b/web/pixel-server/PixelServer/Startup.cs @@ -26,7 +26,8 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddMemoryCache(); - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + services.AddApplicationInsightsTelemetry(Configuration); + services.AddMvcCore(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/web/pixel-server/PixelServer/appsettings.json b/web/pixel-server/PixelServer/appsettings.json index f5127177441..95ab1485719 100644 --- a/web/pixel-server/PixelServer/appsettings.json +++ b/web/pixel-server/PixelServer/appsettings.json @@ -8,4 +8,4 @@ "ApplicationInsights": { "InstrumentationKey": "SetInAzure" } -} \ No newline at end of file +} diff --git a/web/pixel-server/README.md b/web/pixel-server/README.md new file mode 100644 index 00000000000..feea12bf57e --- /dev/null +++ b/web/pixel-server/README.md @@ -0,0 +1,52 @@ +# Pixel Server + +This site is intended as a backing server to serve pixels being loaded on a web page. Note that the limit of data retrieved here is merely: + +> a pixel with query string `x` was requested + +No PII data is recorded. + +## Prerequisites +* This project was built, tested, and peformance checked using `.NET Core 2.1`. +* It is intended (and has been tested) for deployment to an `Azure AppService` +* The backing data store is `Application Insights`, so you will need to create one prior to deployment. + +## Usage +To add a tracking pixel, simply embed an image of the following form. + +``` +Markdown: +![AltText](https://.azurewebsites.net/api/impressions?path= "AltText") + +RST: +.. image:: https://.azurewebsites.net/api/impressions?path= + +Img URL: + +``` + +## Deployment and Setup + +This site is currently configured to leverage `Application Insights` as a connected service. With the code that is currently checked in, the order to deploy your own version of this site is as follows: + +* Deploy site in `release` configuration to your AppService +* Enable Application Insights on your AppService + +## Enabling Application Insights on your AppService + +First, install the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest). Use the `az login` command to sign in with the appropriate email to access your azure subscriptions. After that, ensure you've created an `Application Insights` where it is visible to your target AppService. + +Then, leverage the following az commands: + +``` +#retrieve the InstrumentationKey for your previously created appinsights +$key = az resource show -g -n --resource-type "Microsoft.Insights/components" --query properties.InstrumentationKey + +#set the application insight for web app +az webapp config appsettings set -g -n --settings "APPINSIGHTS_INSTRUMENTATIONKEY = $key" +``` + +Or, if you the above is too much trouble. Go through the UI. + +![Where To Click](example.png) + diff --git a/web/pixel-server/example.png b/web/pixel-server/example.png new file mode 100644 index 0000000000000000000000000000000000000000..3b2360ee1c8ff06dc9f678a56722cce2b6168aa9 GIT binary patch literal 34849 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