otel for call start, end and mute
This is send over zipkin. And it uses a posthog exporter to export events to posthog using a _otel prefix
This commit is contained in:
57
src/analytics/OtelPosthogExporter.ts
Normal file
57
src/analytics/OtelPosthogExporter.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { SpanExporter } from "@opentelemetry/sdk-trace-base";
|
||||
import { ReadableSpan } from "@opentelemetry/sdk-trace-base";
|
||||
import { ExportResult, ExportResultCode } from "@opentelemetry/core";
|
||||
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
/**
|
||||
* This is implementation of {@link SpanExporter} that prints spans to the
|
||||
* console. This class can be used for diagnostic purposes.
|
||||
*/
|
||||
export class PosthogSpanExporter implements SpanExporter {
|
||||
/**
|
||||
* Export spans.
|
||||
* @param spans
|
||||
* @param resultCallback
|
||||
*/
|
||||
async export(
|
||||
spans: ReadableSpan[],
|
||||
resultCallback: (result: ExportResult) => void
|
||||
): Promise<void> {
|
||||
console.log("POSTHOGEXPORTER", spans);
|
||||
for (let i = 0; i < spans.length; i++) {
|
||||
const span = spans[i];
|
||||
const sendInstantly =
|
||||
span.name == "otel_callEnded" ||
|
||||
span.name == "otel_otherSentInstantlyEventName";
|
||||
|
||||
await PosthogAnalytics.instance.trackFromSpan(
|
||||
{ eventName: span.name, ...span.attributes },
|
||||
{
|
||||
send_instantly: sendInstantly,
|
||||
}
|
||||
);
|
||||
resultCallback({ code: ExportResultCode.SUCCESS });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Shutdown the exporter.
|
||||
*/
|
||||
shutdown(): Promise<void> {
|
||||
console.log("POSTHOGEXPORTER shutdown of otelPosthogExporter");
|
||||
return new Promise<void>((resolve, _reject) => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* converts span info into more readable format
|
||||
* @param span
|
||||
*/
|
||||
// private _exportInfo;
|
||||
/**
|
||||
* Showing spans in console
|
||||
* @param spans
|
||||
* @param done
|
||||
*/
|
||||
// private _sendSpans;
|
||||
}
|
||||
//# sourceMappingURL=ConsoleSpanExporter.d.ts.map
|
||||
@@ -385,6 +385,22 @@ export class PosthogAnalytics {
|
||||
this.capture(eventName, properties, options);
|
||||
}
|
||||
|
||||
public async trackFromSpan(
|
||||
{ eventName, ...properties },
|
||||
options?: CaptureOptions
|
||||
): Promise<void> {
|
||||
if (this.identificationPromise) {
|
||||
// only make calls to posthog after the identificaion is done
|
||||
await this.identificationPromise;
|
||||
}
|
||||
if (
|
||||
this.anonymity == Anonymity.Disabled ||
|
||||
this.anonymity == Anonymity.Anonymous
|
||||
)
|
||||
return;
|
||||
this.capture(eventName, properties, options);
|
||||
}
|
||||
|
||||
public startListeningToSettingsChanges(): void {
|
||||
// Listen to account data changes from sync so we can observe changes to relevant flags and update.
|
||||
// This is called -
|
||||
|
||||
@@ -35,6 +35,7 @@ import { useLocationNavigation } from "../useLocationNavigation";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||
import { findDeviceByName, getDevices } from "../media-utils";
|
||||
import { callTracer } from "../telemetry/otel";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -143,7 +144,7 @@ export function GroupCallView({
|
||||
]);
|
||||
|
||||
await groupCall.enter();
|
||||
|
||||
callTracer.startCall(groupCall.groupCallId);
|
||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
||||
|
||||
@@ -164,6 +165,7 @@ export function GroupCallView({
|
||||
if (isEmbedded && !preload) {
|
||||
// In embedded mode, bypass the lobby and just enter the call straight away
|
||||
groupCall.enter();
|
||||
callTracer.startCall(groupCall.groupCallId);
|
||||
|
||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
||||
@@ -187,6 +189,7 @@ export function GroupCallView({
|
||||
|
||||
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
|
||||
// therefore we want the event to be sent instantly without getting queued/batched.
|
||||
callTracer.endCall();
|
||||
const sendInstantly = !!widget;
|
||||
PosthogAnalytics.instance.eventCallEnded.track(
|
||||
groupCall.groupCallId,
|
||||
|
||||
@@ -32,6 +32,7 @@ import { usePageUnload } from "./usePageUnload";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { TranslatedError, translatedError } from "../TranslatedError";
|
||||
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
|
||||
import { callTracer } from "../telemetry/otel";
|
||||
|
||||
export enum ConnectionState {
|
||||
EstablishingCall = "establishing call", // call hasn't been established yet
|
||||
@@ -375,6 +376,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
callTracer.startCall(groupCall.groupCallId);
|
||||
|
||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
||||
@@ -399,6 +401,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||
const setMicrophoneMuted = useCallback(
|
||||
(setMuted) => {
|
||||
groupCall.setMicrophoneMuted(setMuted);
|
||||
callTracer.muteMic(setMuted);
|
||||
PosthogAnalytics.instance.eventMuteMicrophone.track(
|
||||
setMuted,
|
||||
groupCall.groupCallId
|
||||
|
||||
128
src/telemetry/otel.ts
Normal file
128
src/telemetry/otel.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/* document-load.ts|js file - the code is the same for both the languages */
|
||||
import {
|
||||
ConsoleSpanExporter,
|
||||
SimpleSpanProcessor,
|
||||
} from "@opentelemetry/sdk-trace-base";
|
||||
import { ZipkinExporter } from "@opentelemetry/exporter-zipkin";
|
||||
// import { JaegerExporter } from "@opentelemetry/exporter-jaeger";
|
||||
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
||||
import { ZoneContextManager } from "@opentelemetry/context-zone";
|
||||
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
||||
import opentelemetry from "@opentelemetry/api";
|
||||
import { Resource } from "@opentelemetry/resources";
|
||||
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
|
||||
|
||||
import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter";
|
||||
|
||||
const SERVICE_NAME = "element-call";
|
||||
// It is really important to set the correct content type here. Otherwise the Jaeger will crash and not accept the zipkin event
|
||||
// Additionally jaeger needs to be started with zipkin on port 9411
|
||||
const optionsZipkin = {
|
||||
// url: `http://localhost:9411/api/v2/spans`,
|
||||
// serviceName: SERVICE_NAME,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
// We DO NOT use the OTLPTraceExporter. This somehow does not hit the right endpoint and also causes issues with CORS
|
||||
const collectorOptions = {
|
||||
// url: `http://localhost:14268/api/v2/spans`, // url is optional and can be omitted - default is http://localhost:4318/v1/traces
|
||||
headers: { "Access-Control-Allow-Origin": "*" }, // an optional object containing custom headers to be sent with each request
|
||||
concurrencyLimit: 10, // an optional limit on pending requests
|
||||
};
|
||||
const otlpExporter = new OTLPTraceExporter(collectorOptions);
|
||||
const consoleExporter = new ConsoleSpanExporter();
|
||||
// The zipkin exporter is the actual exporter we need for web based otel applications
|
||||
const zipkin = new ZipkinExporter(optionsZipkin);
|
||||
const posthogExporter = new PosthogSpanExporter();
|
||||
|
||||
// This is how we can make Jaeger show a reaonsable service in the dropdown on the left.
|
||||
const providerConfig = {
|
||||
resource: new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
|
||||
}),
|
||||
};
|
||||
const provider = new WebTracerProvider(providerConfig);
|
||||
|
||||
provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter));
|
||||
// We can add as many processors and exporters as we want to. The zipkin one is the important one for Jaeger
|
||||
provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter));
|
||||
provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter));
|
||||
provider.addSpanProcessor(new SimpleSpanProcessor(zipkin));
|
||||
|
||||
// This is unecassary i think...
|
||||
provider.register({
|
||||
// Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional
|
||||
contextManager: new ZoneContextManager(),
|
||||
});
|
||||
|
||||
// Registering instrumentations (These are automated span collectors for the Http request during page loading, switching)
|
||||
registerInstrumentations({
|
||||
instrumentations: [
|
||||
// new DocumentLoadInstrumentation(),
|
||||
// new UserInteractionInstrumentation(),
|
||||
],
|
||||
});
|
||||
|
||||
// This is not the serviceName shown in jaeger
|
||||
export const tracer = opentelemetry.trace.getTracer(
|
||||
"my-element-call-otl-tracer"
|
||||
);
|
||||
|
||||
class CallTracer {
|
||||
// We create one tracer class for each main context.
|
||||
// Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately.
|
||||
// The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates.
|
||||
// Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call.
|
||||
|
||||
private callSpan;
|
||||
private callContext;
|
||||
private muteSpan?;
|
||||
public startCall(callId: string) {
|
||||
// The main context will be set when initiating the main/parent span.
|
||||
|
||||
// Create an initial context with the callId param
|
||||
const callIdContext = opentelemetry.context
|
||||
.active()
|
||||
.setValue(Symbol("callId"), callId);
|
||||
|
||||
// Create the main span that tracks the whole call
|
||||
this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext);
|
||||
|
||||
// Create a new call based on the callIdContext. This context also has a span assigned to it.
|
||||
// Other spans can use this context to extract the parent span.
|
||||
// (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent)
|
||||
this.callContext = opentelemetry.trace.setSpan(
|
||||
opentelemetry.context.active(),
|
||||
this.callSpan
|
||||
);
|
||||
|
||||
// Here we start a very short span. This is a hack to trigger the posthog exporter.
|
||||
// Only ended spans are processed by the exporter.
|
||||
// We want the exporter to know that a call has started
|
||||
const startCallSpan = tracer.startSpan(
|
||||
"otel_startCallSpan",
|
||||
undefined,
|
||||
this.callContext
|
||||
);
|
||||
startCallSpan.end();
|
||||
}
|
||||
public muteMic(muteState: boolean) {
|
||||
if (muteState) {
|
||||
this.muteSpan = tracer.startSpan(
|
||||
"otel_muteSpan",
|
||||
undefined,
|
||||
this.callContext
|
||||
);
|
||||
} else if (this.muteSpan) {
|
||||
this.muteSpan.end();
|
||||
this.muteSpan = null;
|
||||
}
|
||||
}
|
||||
public endCall() {
|
||||
this.callSpan?.end();
|
||||
}
|
||||
}
|
||||
|
||||
export const callTracer = new CallTracer();
|
||||
Reference in New Issue
Block a user