Skip to content

Commit

Permalink
feat: Redact anonymous attributes within feature events
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 committed Jan 18, 2024
1 parent e1f8521 commit 28bbcc8
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 7 deletions.
3 changes: 2 additions & 1 deletion ContractTests/Source/Controllers/SdkController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ final class SdkController: RouteCollection {
"auto-env-attributes",
"context-comparison",
"etag-caching",
"inline-context"
"inline-context",
"anonymous-redaction"
]

return StatusResponse(
Expand Down
28 changes: 24 additions & 4 deletions LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,28 @@ public struct LDContext: Encodable, Equatable {
fileprivate var canonicalizedKey: String
internal var attributes: [String: LDValue] = [:]

// Internal property used to control encoding.
//
// Normally we would control this encoding mechanism through the use of userKeys.
// However, the anonymous redaction mechanism has to vary on a per event basis,
// and so we cannot affect the entire JSON encoder in this way.
internal var redactAnonymousAttributes: Bool = false

fileprivate init(canonicalizedKey: String) {
self.canonicalizedKey = canonicalizedKey
}

internal init(copyFrom: LDContext) {
kind = copyFrom.kind
contexts = copyFrom.contexts.map { c in LDContext(copyFrom: c) }
name = copyFrom.name
anonymous = copyFrom.anonymous
privateAttributes = copyFrom.privateAttributes
key = copyFrom.key
canonicalizedKey = copyFrom.canonicalizedKey
attributes = copyFrom.attributes
}

init() {
self.init(canonicalizedKey: LDContext.defaultKey(kind: Kind.user))
}
Expand Down Expand Up @@ -100,7 +118,7 @@ public struct LDContext: Encodable, Equatable {
}
}

static private func encodeSingleContext(container: inout KeyedEncodingContainer<DynamicCodingKeys>, context: LDContext, discardKind: Bool, redactAttributes: Bool, allAttributesPrivate: Bool, globalPrivateAttributes: SharedDictionary<String, PrivateAttributeLookupNode>) throws {
static private func encodeSingleContext(container: inout KeyedEncodingContainer<DynamicCodingKeys>, context: LDContext, discardKind: Bool, redactAttributes: Bool, allAttributesPrivate: Bool, globalPrivateAttributes: SharedDictionary<String, PrivateAttributeLookupNode>, redactAnonymousAttributes: Bool) throws {
if !discardKind {
try container.encodeIfPresent(context.kind.description, forKey: DynamicCodingKeys(string: "kind"))
}
Expand All @@ -111,10 +129,12 @@ public struct LDContext: Encodable, Equatable {
var redactedAttributes: [String] = []
redactedAttributes.reserveCapacity(20)

let redactAll = allAttributesPrivate || (context.anonymous && redactAnonymousAttributes)

for key in optionalAttributeNames {
let reference = Reference(key)
if let value = context.getValue(reference) {
if allAttributesPrivate {
if redactAll {
redactedAttributes.append(reference.raw())
continue
}
Expand Down Expand Up @@ -251,10 +271,10 @@ public struct LDContext: Encodable, Equatable {

for context in contexts {
var contextContainer = container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: context.kind.description))
try LDContext.encodeSingleContext(container: &contextContainer, context: context, discardKind: true, redactAttributes: redactAttributes, allAttributesPrivate: allPrivate, globalPrivateAttributes: globalDictionary)
try LDContext.encodeSingleContext(container: &contextContainer, context: context, discardKind: true, redactAttributes: redactAttributes, allAttributesPrivate: allPrivate, globalPrivateAttributes: globalDictionary, redactAnonymousAttributes: redactAnonymousAttributes)
}
} else {
try LDContext.encodeSingleContext(container: &container, context: self, discardKind: false, redactAttributes: redactAttributes, allAttributesPrivate: allPrivate, globalPrivateAttributes: globalDictionary)
try LDContext.encodeSingleContext(container: &container, context: self, discardKind: false, redactAttributes: redactAttributes, allAttributesPrivate: allPrivate, globalPrivateAttributes: globalDictionary, redactAnonymousAttributes: redactAnonymousAttributes)
}
}

Expand Down
12 changes: 10 additions & 2 deletions LaunchDarkly/LaunchDarkly/Models/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,18 @@ class FeatureEvent: Event, SubEvent {
self.value = value
self.defaultValue = defaultValue
self.featureFlag = featureFlag
self.context = context
self.includeReason = includeReason
self.creationDate = creationDate
super.init(kind: isDebug ? .debug : .feature)

if isDebug {
self.context = context
super.init(kind: .debug)
} else {
var newContext = LDContext(copyFrom: context)
newContext.redactAnonymousAttributes = true
self.context = newContext
super.init(kind: .feature)
}
}

fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer<Event.CodingKeys>) throws {
Expand Down

0 comments on commit 28bbcc8

Please sign in to comment.