Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.datadog.android.rum.RumErrorSource
import com.datadog.android.rum.RumResourceKind
import com.datadog.android.rum.RumResourceMethod
import com.datadog.android.rum.featureoperations.FailureReason
import com.datadog.android.rum.model.ResourceEvent
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
Expand Down Expand Up @@ -191,11 +192,20 @@ class DdRumImplementation(private val datadog: DatadogWrapper = DatadogSDKWrappe
val attributes = context.toHashMap().toMutableMap().apply {
put(RumAttributes.INTERNAL_TIMESTAMP, timestampMs.toLong())
}

val resourceSize = if (size.toLong() == MISSING_RESOURCE_SIZE) {
null
} else {
size.toLong()
}

val rawGraphqlErrors = context.toHashMap()[RumAttributes.GRAPHQL_ERRORS]
val graphqlErrors = GraphqlParser.parse(rawGraphqlErrors)

if (graphqlErrors != null) {
attributes.put(RumAttributes.GRAPHQL_ERRORS, graphqlErrors as Any)
}

datadog.getRumMonitor().stopResource(
key = key,
statusCode = statusCode.toInt(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
package com.datadog.reactnative

import com.datadog.android.rum.model.ResourceEvent

/**
* Parses GraphQL data from React Native into ResourceEvent errors.
*/
object GraphqlParser {
/**
* Accepts the raw value stored under "_dd.graphql.errors".
*
* Expected RN shape:
* - Map { "errors": [ { "message": "...", "code": "...", "path": [...], "locations": [...] }, ... ] }
*
* Returns null if missing / invalid.
*/
fun parse(rawGraphqlErrors: Any?): List<ResourceEvent.Error>? {
val wrapper = rawGraphqlErrors as? Map<*, *> ?: return null
val errorsList = wrapper["errors"] as? List<*> ?: return null

val parsed = errorsList.mapNotNull { parseError(it) }
return parsed.takeIf { it.isNotEmpty() }
}

private fun parseError(item: Any?): ResourceEvent.Error? {
val m = item as? Map<*, *> ?: return null

val message = m["message"] as? String ?: return null
val code = m["code"] as? String

val locations = parseLocations(m["locations"])
val path = parsePath(m["path"])

return ResourceEvent.Error(
message = message,
code = code,
locations = locations,
path = path
)
}

private fun parseLocations(raw: Any?): List<ResourceEvent.Location>? {
val list = raw as? List<*> ?: return null
val parsed = list.mapNotNull { parseLocation(it) }
return parsed.takeIf { it.isNotEmpty() }
}

private fun parseLocation(locAny: Any?): ResourceEvent.Location? {
val locMap = locAny as? Map<*, *> ?: return null
val line = (locMap["line"] as? Number)?.toLong() ?: return null
val column = (locMap["column"] as? Number)?.toLong() ?: return null
return ResourceEvent.Location(line = line, column = column)
}

private fun parsePath(raw: Any?): List<ResourceEvent.Path>? {
val list = raw as? List<*> ?: return null
val parsed = list.mapNotNull { seg ->
when (seg) {
is String -> ResourceEvent.Path.String(seg)
is Number -> ResourceEvent.Path.Long(seg.toLong())
else -> null
}
}
return parsed.takeIf { it.isNotEmpty() }
}
}
15 changes: 11 additions & 4 deletions packages/core/ios/Sources/AnyEncodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@

import Foundation

internal func castAttributesToSwift(_ attributes: NSDictionary) -> [String: Encodable] {
return castAttributesToSwift(attributes as? [String: Any] ?? [:])
internal func castAttributesToSwift(_ attributes: NSDictionary, _ keys: [String]? = nil) -> [String: Encodable] {
return castAttributesToSwift(attributes as? [String: Any] ?? [:], keys)
}

internal func castAttributesToSwift(_ attributes: [String: Any]) -> [String: Encodable] {
internal func castAttributesToSwift(_ attributes: [String: Any], _ keys: [String]? = nil) -> [String: Encodable] {
var casted: [String: Encodable] = [:]
casted.reserveCapacity(attributes.count)

attributes.forEach { key, value in
casted[key] = castValueToSwift(value)
if let keys, keys.contains(key),
JSONSerialization.isValidJSONObject(value),
let data = try? JSONSerialization.data(withJSONObject: value) {
casted[key] = data
} else {
casted[key] = castValueToSwift(value)
}
}

return casted
Expand Down
25 changes: 25 additions & 0 deletions packages/core/ios/Sources/Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ internal struct CrossPlatformAttributes {
/// and send it within the RUM resource, so the RUM backend can issue corresponding APM span on behalf of the mobile app.
/// Expects `String` value.
static let spanID = "_dd.span_id"

/// Custom attribute passed when starting GraphQL RUM resources from a cross platform SDK.
/// It sets the GraphQL operation name if it was defined by the developer.
/// Expects `String` value.
public static let graphqlOperationName = "_dd.graphql.operation_name"

/// Custom attribute passed when starting GraphQL RUM resources from a cross platform SDK.
/// It sets the GraphQL operation type.
/// Expects `String` value of either `query`, `mutation` or `subscription`.
public static let graphqlOperationType = "_dd.graphql.operation_type"

/// Custom attribute passed when starting GraphQL RUM resources from a cross platform SDK.
/// It sets the GraphQL payload as a JSON string when it is specified.
/// Expects `String` value.
public static let graphqlPayload = "_dd.graphql.payload"

/// Custom attribute passed when starting GraphQL RUM resources resources from a cross platform SDK.
/// It sets the GraphQL variables as a JSON string if they were defined by the developer.
/// Expects `String` value.
public static let graphqlVariables = "_dd.graphql.variables"

/// Custom attribute passed when completing GraphQL RUM resources that contain errors in the response.
/// It sets the GraphQL errors from the response body as JSON data.
/// Expects `Data` value.
public static let graphqlErrors = "_dd.graphql.errors"
}

/// Internal attributes used to configure the proxy.
Expand Down
8 changes: 5 additions & 3 deletions packages/core/ios/Sources/DdRumImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,14 @@ public class DdRumImplementation: NSObject {
addResourceMetrics(key: key, resourceTimings: resourceTimings)
}

let dataKeys: [String] = [CrossPlatformAttributes.graphqlErrors]

nativeRUM.stopResource(
resourceKey: key,
statusCode: Int(statusCode),
kind: RUMResourceType(from: kind),
size: Int64(size) == Self.missingResourceSize ? nil : Int64(size),
attributes: attributes(from: mutableContext, with: timestampMs)
attributes: attributes(from: mutableContext, with: timestampMs, keys: dataKeys)
)
resolve(nil)
}
Expand Down Expand Up @@ -290,10 +292,10 @@ public class DdRumImplementation: NSObject {

// MARK: - Private methods

private func attributes(from context: NSDictionary, with timestampMs: Double) -> [String: Encodable] {
private func attributes(from context: NSDictionary, with timestampMs: Double, keys: [String]? = nil) -> [String: Encodable] {
var context = context as? [String: Any] ?? [:]
context[Self.timestampKey] = Int64(timestampMs)
return castAttributesToSwift(context)
return castAttributesToSwift(context, keys)
}

private func addResourceMetrics(key: String, resourceTimings: [String: Any]) {
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/DdAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,12 @@ export const DdAttributes = {
* The Debug ID establishes a unique connection between a bundle and its corresponding sourcemap.
* Expects {@link String} value.
*/
debugId: '_dd.debug_id'
debugId: '_dd.debug_id',

/**
* Custom attribute passed when completing GraphQL RUM resources that contain errors in the response.
* It sets the GraphQL errors from the response body as JSON string.
* Expects {@link String} value (JSON serialized errors array).
*/
graphqlErrors: '_dd.graphql.errors'
};
6 changes: 5 additions & 1 deletion packages/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ import {
import {
DATADOG_GRAPH_QL_OPERATION_NAME_HEADER,
DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER,
DATADOG_GRAPH_QL_VARIABLES_HEADER
DATADOG_GRAPH_QL_VARIABLES_HEADER,
DATADOG_GRAPH_QL_PAYLOAD_HEADER,
DATADOG_GRAPH_QL_ERROR_HEADER
} from './rum/instrumentation/resourceTracking/graphql/graphqlHeaders';
import type { FirstPartyHost } from './rum/types';
import { PropagatorType, RumActionType } from './rum/types';
Expand Down Expand Up @@ -82,6 +84,8 @@ export {
DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER,
DATADOG_GRAPH_QL_OPERATION_NAME_HEADER,
DATADOG_GRAPH_QL_VARIABLES_HEADER,
DATADOG_GRAPH_QL_PAYLOAD_HEADER,
DATADOG_GRAPH_QL_ERROR_HEADER,
TracingIdType,
TracingIdFormat,
DatadogTracingIdentifier,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { DATADOG_CUSTOM_HEADER_PREFIX } from '../headers';

/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
import { DATADOG_CUSTOM_HEADER_PREFIX } from '../headers';

export const DATADOG_GRAPH_QL_OPERATION_NAME_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-name`;
export const DATADOG_GRAPH_QL_VARIABLES_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-variables`;
export const DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-type`;
export const DATADOG_GRAPH_QL_PAYLOAD_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-payload`;
export const DATADOG_GRAPH_QL_ERROR_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-error`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

/**
* Extracts and filters GraphQL errors from an errors array.
* Only extracts: message, code, locations, path
*/
export function extractGraphQLErrors(
errors: any[]
): {
message: string;
code?: string;
locations?: Array<{ line: number; column: number }>;
path?: Array<string | number>;
}[] {
return errors
.filter((error: any) => error && error.message) // Skip errors without message
.map((error: any) => {
const filtered: any = {
message: String(error.message) // Ensure it's a string
};

// Extract code from extensions.code (preferred) or legacy top-level code
const code = error.extensions?.code ?? error.code;
if (code) {
filtered.code = String(code);
}

// Extract locations array if present
if (error.locations && Array.isArray(error.locations)) {
filtered.locations = error.locations
.filter(
(loc: any) =>
loc &&
typeof loc.line === 'number' &&
typeof loc.column === 'number'
)
.map((loc: any) => ({
line: loc.line,
column: loc.column
}));
}

// Extract path array if present (can contain strings or numbers)
if (error.path && Array.isArray(error.path)) {
filtered.path = error.path;
}

return filtered;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ const formatResourceStopContext = (
if (graphqlAttributes.variables) {
attributes['_dd.graphql.variables'] = graphqlAttributes.variables;
}

if (graphqlAttributes.payload) {
attributes['_dd.graphql.payload'] = graphqlAttributes.payload;
}

if (graphqlAttributes.errors) {
attributes['_dd.graphql.errors'] = {
errors: graphqlAttributes.errors
};
}
}

return attributes;
Expand Down
Loading