IntegrationsData WarehousesAzure Data Explorer

Azure Data Explorer

Datafly Signal writes events directly into an Azure Data Explorer (Kusto) table so you can query first-party behaviour using KQL.

Why route Azure Data Explorer through Datafly Signal

  • The data lands in infrastructure you own. Signal writes to your Azure Data Explorer directly from your own deployment. Datafly operates no intermediary and never holds a copy of your event data.
  • Client IP and user agent are recorded as event context. Both come from the original visitor request rather than from your server, so geography and device analysis done in the warehouse reflects the actual visitor.
  • Presets for eight verticals. General (the default: page, identify and the key cross-vertical events), Retail, Insurance, Travel, Media & Streaming, Publishing, B2B / SaaS and Automotive, each writing one named field per documented event property. Email, phone and names are written only as SHA-256 digests unless you deliberately change the installed blueprint.
  • One consent decision governs every destination. Azure Data Explorer is gated by the same consent state as the rest of your stack, so a withdrawal applies everywhere at once.

Prerequisites

Before configuring Azure Data Explorer in Signal, you need an Azure account with an ADX cluster, a database and table, and an Azure AD app registration for authentication.

Create an Azure Account

If you don’t already have one, sign up at azure.microsoft.com.

Create an Azure Data Explorer Cluster

  1. In the Azure portal, search for Azure Data Explorer Clusters and click Create.
  2. Enter a Cluster name (e.g. datafly-adx).
  3. Select a Region close to your Signal deployment.
  4. Choose a Compute specification based on your expected data volume.
  5. Click Review + Create > Create.
  6. Once deployed, note the URI (e.g. https://datafly-adx.westeurope.kusto.windows.net).

Create a Database

  1. Open your ADX cluster in the Azure portal.
  2. Click Databases in the left sidebar.
  3. Click Add database.
  4. Enter a Database name (e.g. analytics).
  5. Set the Retention period and Cache period as needed.
  6. Click Create.

Create a Table

  1. In the ADX cluster, go to Query in the left sidebar.
  2. Select your database and run the following KQL command:
.create table SignalEvents (
  message_id: string,
  event_id: string,
  event: string,
  type: string,
  anonymous_id: string,
  user_id: string,
  device_id: string,
  timestamp: datetime,
  sent_at: datetime,
  received_at: datetime,
  org_id: string,
  pipeline_id: string,
  vendor_type: string,
  properties: dynamic,
  context: dynamic,
  click_ids: dynamic,
  collected_ids: dynamic
)
  1. Create a JSON ingestion mapping for it (streaming ingestion cannot take an inline mapping):
.create table SignalEvents ingestion json mapping 'SignalEventsMapping' '[{"column":"message_id","Properties":{"Path":"$.message_id"}},{"column":"event_id","Properties":{"Path":"$.event_id"}},{"column":"event","Properties":{"Path":"$.event"}},{"column":"type","Properties":{"Path":"$.type"}},{"column":"anonymous_id","Properties":{"Path":"$.anonymous_id"}},{"column":"user_id","Properties":{"Path":"$.user_id"}},{"column":"device_id","Properties":{"Path":"$.device_id"}},{"column":"timestamp","Properties":{"Path":"$.timestamp"}},{"column":"sent_at","Properties":{"Path":"$.sent_at"}},{"column":"received_at","Properties":{"Path":"$.received_at"}},{"column":"org_id","Properties":{"Path":"$.org_id"}},{"column":"pipeline_id","Properties":{"Path":"$.pipeline_id"}},{"column":"vendor_type","Properties":{"Path":"$.vendor_type"}},{"column":"properties","Properties":{"Path":"$.properties"}},{"column":"context","Properties":{"Path":"$.context"}},{"column":"click_ids","Properties":{"Path":"$.click_ids"}},{"column":"collected_ids","Properties":{"Path":"$.collected_ids"}}]'
  1. Enable streaming ingestion on the table (or on the database). Signal uses the streaming ingest API, so this is required, as is Streaming ingestion: On in the cluster’s Configurations:
.alter table SignalEvents policy streamingingestion enable

Register an Azure AD Application

  1. In the Azure portal, go to Azure Active Directory > App registrations > New registration.
  2. Enter a name (e.g. datafly-signal-adx).
  3. Click Register.
  4. Note the Application (client) ID and Directory (tenant) ID from the overview page.

Create a Client Secret

  1. On the app registration page, go to Certificates & secrets > Client secrets > New client secret.
  2. Enter a description and expiry period.
  3. Click Add.
  4. Copy the secret value immediately — it is only shown once.
⚠️

Store the client secret securely. Set a calendar reminder to rotate it before expiry to avoid service interruptions.

Grant Database Permissions

  1. In the ADX cluster, go to Databases > your database > Permissions.
  2. Click Add > Ingestor.
  3. Search for your Azure AD application name and select it.
  4. Click Select.

Configuration

FieldTypeRequiredDescription
cluster_urlstringYesThe cluster URI, for example https://mycluster.westeurope.kusto.windows.net. Also accepts endpoint.
databasestringYesThe database.
table_namestringYesThe table.
ingestion_mappingstringNoName of the JSON ingestion mapping on the table (for example SignalEventsMapping). Without one, top-level JSON fields map to columns of the same name.
client_idstringYesMicrosoft Entra application (client) ID with the Ingestor role on the database.
client_secretsecretYesThe application’s client secret.
tenant_idstringYesThe Microsoft Entra tenant (directory) ID.

Signal Setup

Quick Setup

  1. Navigate to Integrations in the sidebar.
  2. Open the Integration Library tab.
  3. Find Azure Data Explorer or filter by Warehouse.
  4. Click Install, select a variant if available, and fill in the required fields.
  5. Click Install Integration to create the integration with a ready-to-use default blueprint.

API Setup

curl -X POST http://localhost:8084/v1/admin/integration-catalog/azure_data_explorer/install \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Azure Data Explorer",
    "variant": "default",
    "config": {
      "cluster_url": "https://datafly-adx.westeurope.kusto.windows.net",
      "database": "analytics",
      "table_name": "SignalEvents",
      "ingestion_mapping": "SignalEventsMapping",
      "client_id": "00000000-0000-0000-0000-000000000000",
      "client_secret": "YOUR_CLIENT_SECRET",
      "tenant_id": "00000000-0000-0000-0000-000000000000"
    },
    "delivery_mode": "server_side"
  }'

What Signal writes

Each event is one streaming ingestion request: POST <cluster>/v1/rest/ingest/<database>/<table>?streamFormat=Json&mappingName=<ingestion_mapping>, gzip-compressed, with a bearer token for the Entra application. The body is the Signal event as one JSON record (the same document the streaming destinations send):

FieldContents
event, typeEvent name (purchase, page, …) and type (track, page, identify, …).
anonymous_id, user_id, device_idVisitor identifiers (user_id and device_id only when set).
message_id, event_idSignal’s message ID (use it to deduplicate) and the client event ID.
timestamp, sent_at, received_atRFC 3339 UTC times: event, client send, Signal receipt.
org_id, pipeline_id, vendor_typeWhere the event came from and which destination type wrote it.
contextThe collected context as sent: ip, user_agent, locale, page, campaign, consent, geo, device, library, and any partner attributes. Written in full; the blueprint does not filter it (#1414).
click_ids, collected_idsAd click IDs and ID-provider values, when present.
propertiesOnly the fields the blueprint maps (below).

Only what the blueprint maps is written. The properties object holds the fields your installed blueprint maps and nothing else (the row contract): the global block under @root (copies of the envelope fields, UTM parameters, the analytics and marketing consent flags, and email_sha256, phone_sha256, first_name_sha256, last_name_sha256, name_sha256), and the event section’s fields at the top level (transaction_id, value, currency, items, …). Identify traits and client properties the blueprint does not map are never written. An event with no section in the blueprint still arrives, with the @root block only: add a section for any other event whose properties you need.

A streaming ingestion request is limited to 4 MB. Query mapped fields with dot notation, for example SignalEvents | where event == "purchase" | project transaction_id = tostring(properties.transaction_id), value = todouble(properties.value), email_sha256 = tostring(properties["@root"].email_sha256).

The request above was produced by Signal’s Azure Data Explorer writer on 2026-09-23 (batch S1 review) against a local HTTPS endpoint standing in for the cluster; only the Entra ID token was replaced by a static one, since the proof ran with no network access.

Stored event context

Alongside the fields your blueprint maps (where email, phone, names and address are stored as normalised SHA-256 by default), each message carries the event’s collected context exactly as Signal received it. The blueprint does not filter or hash the context: this is deliberate (decision 2026-09-23), so the context is complete for attribution, fraud and debugging queries, and it is listed here so you can decide what to keep.

StoredWhat it contains
Visitor requestIP address, user agent, Accept-Language, locale and timezone
PageURL including its query string, path, title and referrer
CampaignUTM source, medium, campaign, term and content
ConsentThe visitor’s consent state
LocationGeoIP country, region and city, and the network (ASN)
DeviceScreen size; for app events the OS and version, manufacturer, model, advertising ID and IDFV when the app collected them
Vendor IDsCookie and click identifiers in vendor_ids (for example the _fbp browser ID, gclid, ttclid, the GA client ID)
Session and librarySession ID, number and engagement time; the collector’s name and version
Partner attributesProfile attributes from partner enrichment (partner_attrs)

Each message is the event JSON: context, click_ids and collected_ids, and the anonymous_id, user_id, device_id, message_id and event_id identifiers, sit beside the mapped properties.

The IP address, device and advertising IDs, and cookie and click identifiers are personal data under UK and EU GDPR. To keep any of them out of storage, strip, mask or hash the field for every integration with the organisation data layer, apply URL redaction to page URLs, or remove it in the installed blueprint; and set the storage’s retention to match your privacy notice. See Personal data by integration.

The integration’s consent category in Signal decides which events reach Azure Data Explorer; events that fail it are never written. Every row also carries the visitor’s consent state (context.consent / context_json, and consent_analytics / consent_marketing in @root) so downstream queries can apply their own rules.

Testing

  1. Enable the integration in Signal and trigger a test event on your website.
  2. In the ADX cluster, go to Query and select your database.
  3. Run a query to check for events:
SignalEvents
| order by timestamp desc
| take 10
  1. Verify that event rows are appearing with correct data.
  2. In Signal, check the Live Events view to confirm delivery status shows as successful.

Troubleshooting

ProblemSolution
Events not appearing in the tableVerify the cluster URL, database name, and table name are correct. Check that the table exists.
UnauthorizedThe client ID, client secret, or tenant ID is incorrect. Verify the Azure AD app registration details.
ForbiddenThe Azure AD app lacks Ingestor permission on the database. Add the permission in ADX Permissions.
Client secret expiredGenerate a new client secret in Azure AD > App registrations > Certificates & secrets and update the Signal config.
Ingestion latencyStreaming ingestion lands data in seconds. If requests fail with a streaming error, enable Streaming ingestion on the cluster and the streaming ingestion policy on the table or database; schema or mapping changes can take up to five minutes to reach streaming ingestion.
Schema mismatchThe table and the ingestion mapping must match the fields in What Signal writes. Check with .show table SignalEvents schema and .show table SignalEvents ingestion json mappings.

Visit Azure Data Explorer documentation for full KQL reference and ingestion management guides.

See also