Enable privacy protection in the Tealeaf iOS SDK

When capturing user behavior data, you must take measures to exclude personally identifiable information (PII) such as credit card numbers or home addresses.

Before you begin

We recommend assigning accessibility labels (accessibilityLabel) or accessibility IDs (accessibilityIdentifier) to all UI elements that require privacy protection. You can do it programmatically or with the help of the Attribute Inspector in Xcode.

Ways to set accessibility labels and IDs

Tealeaf helps you check which UI elements already have accessibility labels and IDs.

Sample accessibility ID in session replay

Important:

  • Accessibility labels are audible. Accessibility IDs are inaccessible for end users, which makes them useful for internal purposes.
  • Using the same accessibility labels across the iOS and Android versions of an app creates consistency and is considered an industry standard. For more information, see Mobile Accessibility at W3C.
  • If a UI element isn't assigned an accessibility label, accessibility ID or static ID, the library uses its XPath. You can enable privacy protection for the XPath, but it will be necessary to convert it to a regular expression first.

Instructions

  1. Download TealeafLayoutConfig.json from our sample app.
  2. Add the file to your project as a bundle resource. For instructions, see Copy files to the finished product in the Xcode guide.
  3. In TealeafLayoutConfig.json, customize the privacy protection settings for your app.
  4. (recommended) Run the file through a JSON validator.
  5. Add two new sections to the AppDelegate file in your project (Read the new TealeafLayoutConfig settings and Update values in configuration for both JSON objects "AutoLayout" & "AppendMapIds).
import UIKit
import Tealeaf
import EOCore

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let tlfApplicationHelperObj =  TLFApplicationHelper()
        
        // Enable library to load configuration settings
        let appKey:String = "app_key"
        let postMessageURL:String = "endpoint_url"
        tlfApplicationHelperObj.enableTealeafFramework(appKey, withPostMessageUrl: postMessageURL)
        
        // Read the new TealeafLayoutConfig settings
        let tlfAdvFilePath: String? = Bundle.main.path(forResource: "TealeafLayoutConfig", ofType: "json")
        var layoutConfigDict: [AnyHashable : Any] = [:]
        // read data into layoutConfigDict
        loadJson(filePath: tlfAdvFilePath!, jsonDict: &layoutConfigDict)
        
        // Update values in configuration for both json objects "AutoLayout" & "AppendMapIds" in TealeafLayoutConfig.json to values in the frameworks.
        EOApplicationHelper.sharedInstance().setConfigItem("AutoLayout", value:layoutConfigDict["AutoLayout"], forModuleName:kTLFCoreModule)
        EOApplicationHelper.sharedInstance().setConfigItem("AppendMapIds", value:layoutConfigDict["AppendMapIds"], forModuleName:kTLFCoreModule)
        return true
    }
    
    func loadJson(filePath: String, jsonDict:  inout [AnyHashable : Any]) {
        let jsonData = NSData(contentsOfFile: filePath) as Data?
        if let jsonData = jsonData, let json = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [AnyHashable : Any] {
            jsonDict = json
        }
        print("\(filePath):")
        print("\(jsonDict)")
        let error: Error? = nil
        if error != nil {
            print("Error: was not able to load for \(filePath)")
        }
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }
}
#import "AppDelegate.h"

@import Connect;

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  // Enable library to load configuration settings
  NSString *appKey = @"b6c3709b7a4c479bb4b5a9fb8fec324c";
  NSString *postMessageURL =
      @"https://lib-us-2.brilliantcollector.com/collector/collectorPost";
  [[ConnectApplicationHelper sharedInstance] enableFramework:appKey
                                          withPostMessageUrl:postMessageURL];

  // Read the new ConnectLayoutConfig settings
  NSString *tlfAdvFilePath =
      [[NSBundle mainBundle] pathForResource:@"ConnectLayoutConfig"
                                      ofType:@"json"];
  NSMutableDictionary *layoutConfigDict = [NSMutableDictionary dictionary];
  // read data into layoutConfigDict
  [self loadJson:filePath:tlfAdvFilePath jsonDict:&layoutConfigDict];

  // Update values in configuration for both JSON objects "AutoLayout" &
  // "AppendMapIds"
  [[EOApplicationHelper sharedInstance]
      setConfigItem:@"AutoLayout"
              value:layoutConfigDict[@"AutoLayout"]
      forModuleName:kTLFCoreModule];
  [[EOApplicationHelper sharedInstance]
      setConfigItem:@"AppendMapIds"
              value:layoutConfigDict[@"AppendMapIds"]
      forModuleName:kTLFCoreModule];
  return YES;
}

- (void)loadJson:(NSString *)filePath
        jsonDict:(NSMutableDictionary **)jsonDict {
  NSData *jsonData = [NSData dataWithContentsOfFile:filePath];
  if (jsonData) {
    NSError *error = nil;
    id json = [NSJSONSerialization JSONObjectWithData:jsonData
                                              options:0
                                                error:&error];
    if ([json isKindOfClass:[NSDictionary class]]) {
      *jsonDict = [json mutableCopy];
    }
  }
  NSLog(@"%@:", filePath);
  NSLog(@"%@", *jsonDict);
  if (error) {
    NSLog(@"Error: was not able to load for %@", filePath);
  }
}

- (void)applicationWillResignActive:(UIApplication *)application {
  // Sent when the application is about to move from active to inactive state.
  // This can occur for certain types of temporary interruptions (such as an
  // incoming phone call or SMS message) or when the user quits the application
  // and it begins the transition to the background state. Use this method to
  // pause ongoing tasks, disable timers, and throttle down OpenGL ES frame
  // rates. Games should use this method to pause the game.
  [[TLFApplicationHelper sharedInstance] requestManualServerPost];
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
  // Use this method to release shared resources, save user data, invalidate
  // timers, and store enough application state information to restore your
  // application to its current state in case it is terminated later. If your
  // application supports background execution, this method is called instead of
  // applicationWillTerminate: when the user quits.
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
  // Called as part of the transition from the background to the inactive state;
  // here you can undo many of the changes made on entering the background.
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
  // Restart any tasks that were paused (or not yet started) while the
  // application was inactive. If the application was previously in the
  // background, optionally refresh the user interface.
}

- (void)applicationWillTerminate:(UIApplication *)application {
  // Called when the application is about to terminate. Save data if
  // appropriate. See also applicationDidEnterBackground:.
  [[TLFApplicationHelper sharedInstance] requestManualServerPost];
}

@end

How it works

The masking object in TealeafLayoutConfig.json contains all properties related to PII protection.

PropertyNested fieldValuesDescription
HasMaskingBooleanSet the value to true to allow masking.
HasCustomMaskBooleanIf a custom mask is enabled, protected text is replaced with symbols. If not, it appears as blank.
SensitivecapitalCaseAlphabetStringThe symbol to display instead of capital letters. Works if HasCustomMask is set to true.
numberStringThe symbol to display instead of numbers. Works if HasCustomMask is set to true.
smallCaseAlphabetStringThe symbol to display instead of lower-case letters. Works if HasCustomMask is set to true.
symbolStringThe symbol to display instead of other characters. Works if HasCustomMask is set to true.
MaskAccessibilityIdListArray of string The accessibility ID assigned to the UI element (internal)
MaskAccessibilityLabelListArray of stringThe accessibility label assigned to the UI element
MaskIdListArray of stringThe IDs of UI elements.

All XPath IDs must be converted to regular expressions. For example, you can set the regular expression as ^9[0-9][0-9][0-9]$ to match all the controls whose ID has 4 characters.

You can create a general privacy rule that applies to all screens of your app. In that case, use the AutoLayout.GlobalScreenSettings object. To create a rule for a particular screen, add the AutoLayout.ScreenName object where ScreenName is the name of the screen. Feel free to combine general and screen-specific rules within the same configuration file.

{
  "AutoLayout": {
    "GlobalScreenSettings": {
      "Masking": {
        "HasMasking": true,
        "HasCustomMask": true,
        "Sensitive": {
          "capitalCaseAlphabet": "X",
          "number": "9",
          "smallCaseAlphabet": "x",
          "symbol": "#"
        },
        "MaskIdList": [
          "^9[0-9][0-9][0-9]$"
        ],
        "MaskAccessibilityIdList": [
          "loginName",
          "securityQuestion"
        ],
        "MaskAccessibilityLabelList": [
          "Health conditions",
          "Medications taken"
        ]
      }
    }
  },
  "PaymentViewController": {
    "Masking": {
      "HasMasking": true,
      "HasCustomMask": true,
      "Sensitive": {
        "capitalCaseAlphabet": "X",
        "number": "9",
        "smallCaseAlphabet": "x",
        "symbol": "#"
      },
      "MaskIdList": [],
      "MaskAccessibilityIdList": "pii",
      "MaskAccessibilityLabelList": [
        "Card Number",
        "Billing Address"
      ]
    }
  }
}

Testing privacy settings

To make sure your new rule is working correctly, run the app and enter something into the fields you have masked. Then find the session in Tealeaf and check how the input is displayed.