Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
22 changes: 18 additions & 4 deletions PostHog.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
Expand Down Expand Up @@ -63,16 +63,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.AspNetCore", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HogTied.Api", "samples\HogTied.Api\HogTied.Api.csproj", "{97B0F150-CE6A-4AB5-9DCC-221C7E05A0E2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostHog.AI", "src\PostHog.AI\PostHog.AI.csproj", "{03121B36-3191-4F79-8639-D428AE6E985B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostHog.AI.Tests", "tests\PostHog.AI.Tests\PostHog.AI.Tests.csproj", "{A1E6DA80-6150-45E2-B216-80BB7EEF828E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostHog.Example.Console", "samples\PostHog.Example.Console\PostHog.Example.Console.csproj", "{F58C4741-D82A-4EC4-A19B-99BDD7DA1739}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BB9A6B63-4435-4357-AD99-2E43C38E73F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB9A6B63-4435-4357-AD99-2E43C38E73F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
Expand Down Expand Up @@ -102,11 +103,22 @@ Global
{97B0F150-CE6A-4AB5-9DCC-221C7E05A0E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97B0F150-CE6A-4AB5-9DCC-221C7E05A0E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97B0F150-CE6A-4AB5-9DCC-221C7E05A0E2}.Release|Any CPU.Build.0 = Release|Any CPU
{03121B36-3191-4F79-8639-D428AE6E985B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{03121B36-3191-4F79-8639-D428AE6E985B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{03121B36-3191-4F79-8639-D428AE6E985B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{03121B36-3191-4F79-8639-D428AE6E985B}.Release|Any CPU.Build.0 = Release|Any CPU
{A1E6DA80-6150-45E2-B216-80BB7EEF828E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1E6DA80-6150-45E2-B216-80BB7EEF828E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1E6DA80-6150-45E2-B216-80BB7EEF828E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1E6DA80-6150-45E2-B216-80BB7EEF828E}.Release|Any CPU.Build.0 = Release|Any CPU
{F58C4741-D82A-4EC4-A19B-99BDD7DA1739}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F58C4741-D82A-4EC4-A19B-99BDD7DA1739}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F58C4741-D82A-4EC4-A19B-99BDD7DA1739}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F58C4741-D82A-4EC4-A19B-99BDD7DA1739}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BB9A6B63-4435-4357-AD99-2E43C38E73F7} = {9BA7A137-62AD-4837-BE21-B501EF9D66B7}
{8C2AF890-4917-493A-BE14-71405397602A} = {AC5FC694-028B-4AB0-AF76-09726F966A78}
Expand All @@ -118,6 +130,8 @@ Global
{369AE9E5-4FA6-48BF-A35A-03A401DE22AE} = {9BA7A137-62AD-4837-BE21-B501EF9D66B7}
{B16BEF9F-76FC-4362-A7D5-A87DB10933A3} = {9BA7A137-62AD-4837-BE21-B501EF9D66B7}
{97B0F150-CE6A-4AB5-9DCC-221C7E05A0E2} = {5F21F6BA-8ED0-4A96-BDF2-65ABAE0A7A90}
{03121B36-3191-4F79-8639-D428AE6E985B} = {AC5FC694-028B-4AB0-AF76-09726F966A78}
{A1E6DA80-6150-45E2-B216-80BB7EEF828E} = {9BA7A137-62AD-4837-BE21-B501EF9D66B7}
{F58C4741-D82A-4EC4-A19B-99BDD7DA1739} = {5F21F6BA-8ED0-4A96-BDF2-65ABAE0A7A90}
EndGlobalSection
EndGlobal
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ For documentation on the specific packages, see the README files in the respecti
|---------|---------| -----------
| [PostHog.AspNetCore](src/PostHog.AspNetCore/README.md) | [![NuGet version (PostHog.AspNetCore)](https://img.shields.io/nuget/v/PostHog.AspNetCore.svg?style=flat-square)](https://www.nuget.org/packages/PostHog.AspNetCore/) | For use in ASP.NET Core projects.
| [PostHog](src/PostHog/README.md) | [![NuGet version (PostHog)](https://img.shields.io/nuget/v/PostHog.svg?style=flat-square)](https://www.nuget.org/packages/PostHog/) | The core library. Over time, this will support client environments such as Unit, Xamarin, etc.
| [PostHog.AI](src/PostHog.AI/README.md) | [![NuGet version (PostHog.AI)](https://img.shields.io/nuget/v/PostHog.AI.svg?style=flat-square)](https://www.nuget.org/packages/PostHog.AI/) | AI Observability for OpenAI and other LLM providers.

> [!WARNING]
> These packages are currently in a pre-release stage. We're making them available publicly to solicit
Expand All @@ -19,7 +20,7 @@ For documentation on the specific packages, see the README files in the respecti

## Platform

The core [PostHog](./src/PostHog/README.md) package targets `netstandard2.1` and `net8.0` for broad compatibility. The [PostHog.AspNetCore](src/PostHog.AspNetCore/README.md) package targets `net8.0`.
The core [PostHog](./src/PostHog/README.md) package targets `netstandard2.1` and `net8.0` for broad compatibility. The [PostHog.AspNetCore](src/PostHog.AspNetCore/README.md) package targets `net8.0`. The [PostHog.AI](src/PostHog.AI/README.md) package targets `net6.0` for compatibility with .NET 6, 7, and 8 and newer.

## Building

Expand Down
2 changes: 1 addition & 1 deletion samples/HogTied.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// Add services to the container.
builder.AddPostHog(options =>
{
// In general this call is not needed. The default settings are in the "PostHoc" configuration section.
// In general this call is not needed. The default settings are in the "PostHog" configuration section.
// This is here so I can easily switch testing against my local install and production.
options.UseConfigurationSection(builder.Configuration.GetSection("PostHogLocal"));

Expand Down
18 changes: 18 additions & 0 deletions src/PostHog.AI/PostHog.AI.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\PostHog\PostHog.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="8.0.2"
/>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="OpenAI" Version="2.1.0" />
</ItemGroup>
</Project>
177 changes: 177 additions & 0 deletions src/PostHog.AI/PostHogAIContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Threading;

namespace PostHog.AI;

/// <summary>
/// Represents the context for PostHog AI tracking, allowing customization of events within a scope.
/// </summary>
public sealed class PostHogAIContext
{
private static readonly AsyncLocal<PostHogAIContext?> _current = new();

/// <summary>
/// Gets the current PostHog AI context.
/// </summary>
public static PostHogAIContext? Current => _current.Value;

/// <summary>
/// The distinct ID of the user.
/// </summary>
public string? DistinctId { get; }

/// <summary>
/// The trace ID for grouping events.
/// </summary>
public string? TraceId { get; }

/// <summary>
/// The session ID to group related traces together.
/// </summary>
public string? SessionId { get; }

/// <summary>
/// The span ID for this generation event.
/// </summary>
public string? SpanId { get; }

/// <summary>
/// The name for this generation span.
/// </summary>
public string? SpanName { get; }

/// <summary>
/// The parent span ID for tree view grouping.
/// </summary>
public string? ParentId { get; }

/// <summary>
/// Additional properties to include in the event.
/// </summary>
public Dictionary<string, object>? Properties { get; }

/// <summary>
/// Groups to associate with the event.
/// </summary>
public Dictionary<string, object>? Groups { get; }

/// <summary>
/// Whether privacy mode is enabled (e.g., to mask PII).
/// </summary>
public bool? PrivacyMode { get; }

private PostHogAIContext(
string? distinctId,
string? traceId,
string? sessionId,
string? spanId,
string? spanName,
string? parentId,
Dictionary<string, object>? properties,
Dictionary<string, object>? groups,
bool? privacyMode
)
{
DistinctId = distinctId;
TraceId = traceId;
SessionId = sessionId;
SpanId = spanId;
SpanName = spanName;
ParentId = parentId;
Properties = properties;
Groups = groups;
PrivacyMode = privacyMode;
}

/// <summary>
/// Begins a new PostHog AI tracking scope.
/// </summary>
/// <param name="distinctId">Optional distinct ID to associate with events in this scope.</param>
/// <param name="traceId">Optional trace ID to group events.</param>
/// <param name="sessionId">Optional session ID to group related traces together.</param>
/// <param name="spanId">Optional span ID for this generation event.</param>
/// <param name="spanName">Optional name for this generation span.</param>
/// <param name="parentId">Optional parent span ID for tree view grouping.</param>
/// <param name="properties">Optional properties to add to events.</param>
/// <param name="groups">Optional groups to associate with events.</param>
/// <param name="privacyMode">Optional flag to enable/disable privacy mode.</param>
/// <returns>An <see cref="IDisposable"/> that ends the scope when disposed.</returns>
public static IDisposable BeginScope(
string? distinctId = null,
string? traceId = null,
string? sessionId = null,
string? spanId = null,
string? spanName = null,
string? parentId = null,
Dictionary<string, object>? properties = null,
Dictionary<string, object>? groups = null,
bool? privacyMode = null
)
{
// Capture the parent context to restore it later
var parent = _current.Value;

// Create the new context, merging with parent if desired (optional complexity).
// For simplicity and typical "scope" behavior, we can treat this as an override/overlay.
// If we wanted to inherit parent properties, we'd do that merge here.
// Let's stick to "current scope overrides" for now, but if a value is null, we *could* fallback to parent.
// However, explicit "BeginScope" usually implies "Here is the context for this block".
// Let's keep it simple: New scope = New context values.

var newContext = new PostHogAIContext(
distinctId ?? parent?.DistinctId,
traceId ?? parent?.TraceId,
sessionId ?? parent?.SessionId,
spanId ?? parent?.SpanId,
spanName ?? parent?.SpanName,
parentId ?? parent?.ParentId,
MergeDictionaries(parent?.Properties, properties),
MergeDictionaries(parent?.Groups, groups),
privacyMode ?? parent?.PrivacyMode
);

_current.Value = newContext;

return new DisposableScope(parent);
}

private static Dictionary<string, object>? MergeDictionaries(
Dictionary<string, object>? parent,
Dictionary<string, object>? child
)
{
if (parent == null && child == null)
return null;
if (parent == null)
return child;
if (child == null)
return parent;

var merged = new Dictionary<string, object>(parent);
foreach (var kvp in child)
{
merged[kvp.Key] = kvp.Value;
}
return merged;
}

private sealed class DisposableScope : IDisposable
{
private readonly PostHogAIContext? _parent;
private bool _disposed;

public DisposableScope(PostHogAIContext? parent)
{
_parent = parent;
}

public void Dispose()
{
if (_disposed)
return;
_current.Value = _parent;
_disposed = true;
}
}
}
82 changes: 82 additions & 0 deletions src/PostHog.AI/PostHogAIExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.ClientModel;
using System.ClientModel.Primitives;
using Microsoft.Extensions.DependencyInjection;
using OpenAI;

namespace PostHog.AI;

/// <summary>
/// Extension methods for setting up PostHog AI.
/// </summary>
public static class PostHogAIExtensions
{
/// <summary>
/// Adds PostHog AI services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddPostHogAI(this IServiceCollection services)
{
services.AddTransient<PostHogOpenAIHandler>();
return services;
}

/// <summary>
/// Adds the <see cref="PostHogOpenAIHandler"/> to the HTTP client builder.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <returns>The <see cref="IHttpClientBuilder"/>.</returns>
public static IHttpClientBuilder AddPostHogOpenAIHandler(this IHttpClientBuilder builder)
{
return builder.AddHttpMessageHandler<PostHogOpenAIHandler>();
}

/// <summary>
/// Adds an <see cref="OpenAIClient"/> that intercepts requests and sends events to PostHog.
/// Note: This will override the <see cref="OpenAIClientOptions.Transport"/> property to use the PostHog handler.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="apiKey">The OpenAI API key.</param>
/// <param name="configureOptions">Optional action to configure <see cref="OpenAIClientOptions"/>.</param>
/// <returns>The <see cref="IHttpClientBuilder"/> for the underlying HttpClient, allowing further customization (e.g. resilience).</returns>
public static IHttpClientBuilder AddPostHogOpenAIClient(
this IServiceCollection services,
string apiKey,
Action<OpenAIClientOptions>? configureOptions = null
)
{
if (!services.Any(x => x.ServiceType == typeof(IPostHogClient)))
{
throw new InvalidOperationException(
"PostHog services are not registered. Please call 'services.AddPostHog()' before calling 'AddPostHogOpenAIClient'."
);
}

services.AddPostHogAI();

var builder = services.AddHttpClient("PostHogOpenAIClient").AddPostHogOpenAIHandler();

services.AddSingleton<OpenAIClient>(sp =>
{
var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("PostHogOpenAIClient");

var options = new OpenAIClientOptions();
configureOptions?.Invoke(options);

if (options.Transport != null)
{
throw new InvalidOperationException(
"AddPostHogOpenAIClient cannot be used when a custom Transport is set in OpenAIClientOptions. "
+ "To use a custom Transport with PostHog, manually configure the OpenAIClient and add the PostHogOpenAIHandler to your HttpClient."
);
}

options.Transport = new HttpClientPipelineTransport(httpClient);

return new OpenAIClient(new ApiKeyCredential(apiKey), options);
});

return builder;
}
}
Loading