Hide sensitive data

When using autocapture, Heap collects target text information about the element that was interacted with, as well as text data from UI elements further up in an element's ancestry. However, some of these elements might contain sensitive user information, or PII, that needs to be excluded from data capture.

Hiding Sensitive Data on Android

There are three options for hiding sensitive data on Android: Disabling text capture, per-view text redaction, and ignoring interactions for specific views.

Disabling all text capture

When initializing Heap, you have the option to disable text capture for all events that are automatically captured. To do this, set the disableInteractionTextCapture in your options object when calling Heap.startRecording. A similar property is available for disabling capture of all accessibility label text (shown below).

import io.heap.core.Heap
import io.heap.core.Options

Heap.startRecording(
  appContext, 
  "YOUR_ENVIRONMENT_ID", 
  Options(
    disableInteractionTextCapture = true,
    disableInteractionAccessibilityLabelCapture = true
  )
)
import io.heap.core.Heap;
import io.heap.core.Options;

Heap.startRecording(
  appContext, 
  "YOUR_ENVIRONMENT_ID", 
  new Options( // Java uses an overloaded constructor.
		Options.DEFAULT_URI,	// Send data directly to Heap.
    15.0,									// Default upload interval of 15 seconds.
    false,								// Whether to capture advertiser ID. Defaults to false.
    true									// *IMPORTANT* Disable text capture set to true.
  )
);

Redacting text for specific views

If you don't want to disable all text capture for your app, you can take a more targeted approach of only redacting text for specific views in your app that you know might contain sensitive data. When text is redacted for a view, it will still have target text, but the actual text will be replaced with ****. This can be done either in XML or in code, depending on which is most appropriate for your app setup.

If you want to redact text for a view in XML, you can do so by tagging your view with heapRedactText.

<!-- Button clicks are captured but text is redacted. -->
<Button
	android:id="@+id/redact_button"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:text="This text is redacted"
	android:tag="heapRedactText" />

Alternatively, if you want to redact text for a view from your Kotlin or Java code, you can do so using ViewAutocaptureSDK.redactText.

import io.heap.autocapture.ViewAutocaptureSDK

val button = findViewById<Button>(R.id.redact_button)
ViewAutocaptureSDK.redactText(button) // Text in this view will be redacted.
import io.heap.autocapture.ViewAutocaptureSDK;

Button button = (Button)findViewById(R.id.redact_button);
ViewAutocaptureSDK.redactText(button); // Text in this view will be redacted.

Redacting the visible text also redacts the text in the accessibility label to ensure PII isn't leaked when the visible text and the accessibility text are the same value.

Redacting text for specific composables

Similarly to Views, you can redact text on specific composables using the HeapRedact composable provided by the Heap Jetpack Compose Autocapture library. Compose offers more flexibility than Views in that you can redact entire sections of an app, like Views, but you can also un-redact parts of that same section.

Row {
    HeapRedact(redact = true) {
        Button( // This button will be redacted.
            onClick = { /* Some click action */ },
        ) {
            Text(text = "Button 1")
        }
    }
    Button( // This button will NOT be redacted
        onClick = { /* Some click action */ },
    ) {
        Text(text = "Button 2")
    }
}
HeapRedact(redact = true) {
    Row { // This row will be redacted
        Button( // This button will be redacted.
            onClick = { /* Some click action */ },
        ) {
            Text(text = "Button 1")
        }
        Button( // This button will be redacted
            onClick = { /* Some click action */ },
        ) {
            Text(text = "Button 2")
        }
    }
}
HeapRedact(redact = true) {
    Row { // This row will be redacted
        Button( // This button will be redacted.
            onClick = { /* Some click action */ },
        ) {
            Text(text = "Button 1")
        }
        HeapRedact(redact = false) {
            Button( // This button will NOT be redacted
                onClick = { /* Some click action */ },
            ) {
                Text(text = "Button 2")
            }
        }
    }
}

Ignoring all interactions for specific views

If you find that certain interactions are prone to capturing sensitive data, or that a certain interaction might indicate sensitive information about a user during analysis, you have the option of ignoring all interactions on a per-view basis. Again, this can be done in XML or in code.

If you want to ignore all interactions for a view in XML, you can do so by tagging your view with heapIgnoreInteractions.

<!-- Button clicks are not captured. -->
<Button
	android:id="@+id/ignore_button"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:text="Taps on this button are not captured."
	android:tag="heapIgnoreInteractions" />

Alternatively, if you want to flag a view to be ignored from your Kotlin or Java code, you can do so using ViewAutocaptureSDK.ignoreInteractions.

import io.heap.autocapture.ViewAutocaptureSDK

val button = findViewById<Button>(R.id.ignore_button)
ViewAutocaptureSDK.ignoreInteractions(button) // All interactions with this view will be ignored.
import io.heap.autocapture.ViewAutocaptureSDK;

Button button = (Button)findViewById(R.id.ignore_button);
ViewAutocaptureSDK.ignoreInteractions(button); // All interactions with this view will be ignored.

Ignoring all interactions for specific composables

Similar to how text redaction works for Compose, the Heap Jetpack Compose Autocapture library also provides the HeapIgnore composable to allow you to ignore interactions for specific composables or groups of composables.

Row {
    HeapIgnore(ignore = true) {
        Button( // Interactions with this button will be ignored.
            onClick = { /* Some click action */ },
        ) {
            Text(text = "Button 1")
        }
    }
    Button( // This button will NOT be ignored
        onClick = { /* Some click action */ },
    ) {
        Text(text = "Button 2")
    }
}
HeapIgnore(ignore = true) {
    Row { // Interactions in this row will be ignored.
        Button( // This button will be ignored.
            onClick = { /* Some click action */ },
        ) {
            Text(text = "Button 1")
        }
        Button( // This button will be ignored
            onClick = { /* Some click action */ },
        ) {
            Text(text = "Button 2")
        }
    }
}
HeapIgnore(ignore = true) {
    Row { // Interactions in this row will be ignored.
        Button( // This button will be ignored.
            onClick = { /* Some click action */ },
        ) {
            Text(text = "Button 1")
        }
        HeapIgnore(ignore = false) {
            Button( // Interactions on this button will NOT be ignored.
                onClick = { /* Some click action */ },
            ) {
                Text(text = "Button 2")
            }
        }
    }
}

Hiding Sensitive Data on iOS

There are three options for hiding sensitive data on iOS: Disabling the capture of various fields, per-view property redaction, and ignoring events for specific views.

Disabling all text capture

When initializing Heap, you have several options for disabling capture of event properties containing PII:

  • disablePageviewTitleCapture disables capture of pageview titles from navigation and tab items.
  • disableInteractionTextCapture disables capture of user-visible labels from within a view.
  • disableInteractionAccessibilityLabelCapture Disables capture of accessibility labels on views. These labels can be either developer-defined or system generated when a user has accessibility features enabled. It is good practice to use this option when using disableInteractionTextCapture to prevent PII from being captured.

To use these options, pass them into the option dictionary when calling Heap.shared.startRecording.

Heap.shared.startRecording(
  "YOUR_ENVIRONMENT_ID",
  with: [
    .disableInteractionTextCapture: true,
    .disableInteractionAccessibilityLabelCapture: true,
  ]
)
[[Heap sharedInstance]
  startRecording: @"YOUR_ENVIRONMENT_ID"
  withOptions: @{
    HeapOption.disableInteractionTextCapture: @YES,
    HeapOption.disableInteractionAccessibilityLabelCapture: @YES,
  }
];

Redacting text for specific views in UIKit

If you don't want to disable all text capture for your app, you can take a more targeted approach of only redacting text for specific views and view controllers in your app that you know might contain sensitive data. When text is redacted for a view, it will still have target text, but the actual text will be replaced with ****. This can be done either in Interface Builder or in code, depending on which is most appropriate for your app setup.

For view and view controllers, you can set heapRedactText and heapRedactAccessibilityLabel.

To set these properties in Interface Builder, first select the element you want to redact, then open the Attributes Inspector sidebar. Towards the top, you will see a category called Responder with four options. Xcode unfortunately truncates these so you will need to hover over the title to confirm the name. You can set Heap Redact Text and Heap Redact Accessibility Label to On to redact these fields.

Screenshot of the fields in Attributes Inspector.

In code, you simply need to set heapRedactText and heapRedactAccessibilityLabel on view, view controllers, or other responders to redact those properties.

import HeapIOSAutocapture

open override func viewDidLoad() {
  button.heapReactText = true
  button.heapRedactAccessibilityLabel = true
}
@import HeapIOSAutocaptureImplementation;
// Note that this is HeapIOSAutocaptureImplementation and not HeapIOSAutocapture.

- (void)viewDidLoad
{
  self.button.heapReactText = YES;
  self.button.heapRedactAccessibilityLabel = YES;
}

Redacting text for specific views in SwiftUI

Similar to UIKit, you can take a more targeted approach for redaction by only redacting specific views in your app that contain sensitive data. Just like UIKit, redacted text will be replaced **** for the event.

Unlike UIKit, text can be redacted for a given region, as well as a single view, but you can also selectively unmask parts of that region.

NavigationLink {
    TransactionView(transaction: transaction)
} label: {
    HStack {
        Text("btn-outflow-\(transaction.name)") // Not redacted
        Spacer()
        Text(transaction.value) // Redacted
          .foregroundStyle(.red)
          .heapRedactText(true)
    }
}
NavigationLink {
    TransactionView(transaction: transaction)
} label: {
    HStack { // Contents redacted
        Text("btn-outflow-\(transaction.name)") // Redacted
        Spacer()
        Text(transaction.value) // Redacted
          .foregroundStyle(.red)
    }.heapRedactText(true)
}
NavigationLink {
    TransactionView(transaction: transaction)
} label: {
    HStack { // Contents redacted
        Text("btn-outflow-\(transaction.name)") // Redacted
        Spacer()
        Text(transaction.value) // Not redacted
	        .foregroundStyle(.red)
          .heapRedactText(false)
    }.heapRedactText(true)
}

Redacting pageview titles

There currently isn’t a property for disabling pageview title capture. Instead, you can disable capture by overriding heapPageviewTitle on a specific view controller subclass and returning nil for the title.

import HeapIOSAutocapture

extension PrivateViewController {
    open override var heapPageviewTitle: String? {
        nil
    }
}
@import HeapIOSAutocaptureImplementation;
// Note that this is HeapIOSAutocaptureImplementation and not HeapIOSAutocapture.

@implementation PrivateViewController (Heap)

- (nullable NSString *)heapPageviewTitle
{
  return nil;
}

@end

Ignoring all interactions for specific views in UIKit

If you find that certain interactions are prone to capturing sensitive data, or that a certain interaction might indicate sensitive information about a user during analysis, you have the option of ignoring all interactions on a per-view basis. Again, this can be done in Interface Builder or in code.

To set these properties in Interface Builder, first select the element you want to redact, then open the Attributes Inspector sidebar. Towards the top, you will see a category called Responder with four options. Xcode unfortunately truncates these so you will need to hover over the title to confirm the name. You can set Heap Ignore Interactions to On to ignore interactions in this view.

In code, you simply need to set heapIgnoreInteractions on view, view controllers, or other responders to redact those properties.

import HeapIOSAutocapture

open override func viewDidLoad() {
  pinContainerView.heapIgnoreInteractions = true
}
@import HeapIOSAutocaptureImplementation;
// Note that this is HeapIOSAutocaptureImplementation and not HeapIOSAutocapture.

- (void)viewDidLoad
{
  self.pinContainerView.heapIgnoreInteractions = YES;
}

Like with the text redaction properties, these properties are recursive. If, for example, you have a view controller containing a custom PIN screen, you could apply heapIgnoreInteractions to the view or view controller to prevent recording individual button taps.

🚧

It is important to note that this property only applies when to interactions that are hosted within the view or view controller. Interactions in pushed or presented view controllers or elements like UIMenu will not be ignored.

Ignoring all interactions for specific views in SwiftUI

Similar to UIKit, you can also ignore all interactions on specific views in SwiftUI. Also like UIKit, this is applied recursively to all child elements of the ignored view. However, unlike UIKit, it's possible to ignore all interactions in a region, but still selectively capture elements in the ignored region by marking them as not ignored.

VStack(spacing: 20.0) {
  Button(action: {}, label: ...)
  Button(action: {}, label: ...) // Ignored
    .heapIgnoreInteractions(true)
}
VStack(spacing: 20.0) { // Contents ignored
  Button(action: {}, label: ...) // Ignored
  Button(action: {}, label: ...) // Ignored
}.heapIgnoreInteractions(true)
VStack(spacing: 20.0) { // Contents ignored
  Button(action: {}, label: ...) // Ignored
  Button(action: {}, label: ...) // Not ignored
    .heapIgnoreInteractions(false)
}.heapIgnoreInteractions(true)

Hiding Sensitive Data on React Native

There are two options for hiding sensitive data on React Native: Disabling text capture and configurable per-view capture rules.

Disabling all text capture

When initializing Heap, you have the option to disable text capture for all events that are automatically captured. To do this, set the disableInteractionTextCapture in your options object when calling Heap.startRecording. A similar property is available for disabling capture of all accessibility label text (shown below).

Heap.startRecording(
  "YOUR_ENVIRONMENT_ID",
  {
    disableInteractionTextCapture: true,
    disableInteractionAccessibilityLabelCapture: true,
  }
)

Modifying capture for specific views

React Native provides two custom views <HeapIgnore> and <HeapIgnoreText> and a withHeapIgnore higher-order component to restrict some or all event capture in a region.

To see how these are used, consider the following component:

const PinView = (props) => {
  return (
    <View>
      <Button title="1" rounded />
      <Button title="2" rounded />
      ...
    </View>
  );
};

By default, tapping the 1 button will produce events with the hierarchy PinView Button[title=1][rounded=true] and target text of "1".

Below are examples of how to use these components to restrict capture in on that view.

<HeapIgnore>

Wrapping a view with <HeapIgnore> will suppress all events inside the view. For example:

<HeapIgnore>
  <PinView />
</HeapIgnore>

Tapping the buttons in this view will not produce events.

There are 5 props that can be used to expose partial information.

  • allowInteraction - This allows events to be captured with the remaining props determining how much is exposed. Without any other options, the bottom of the hierarchy is the view immediately inside <HeapIgnore>. In the same event from above, the hiearchy would be PinView.
  • allowInnerHierarchy - This allows the full hierarchy to be exposed, but without properties or text capture. In the same event, you would have a hierarchy of PinView Button.
  • allowProps - This allows property capture, but will exclude text-containing properties if the below options are omitted. In the same event, you would have a hierarchy of PinView Button[rounded=true].
  • allowText - This allows text capture. It can be used alongside allowProps to expose text-containing properties.
  • allowAccessibilityLabel - This allows accessibilityLabel capture.

<HeapIgnoreText>

Wrapping a view with <HeapIgnoreText> will capture events but exclude text and accessibility labels from the event. For example:

<HeapIgnoreText>
  <PinView />
</HeapIgnoreText>

Tapping the buttons in this view will produce events but with a hierarchy PinView Button[rounded=true] and no target text. This is equivalent to <HeapIgnore allowInteractions allowInnerHierarchy allowProps>.

withHeapIgnore

withHeapIgnore takes a component and returns a Higher Order Component that wraps it. This component accepts the same arguments but has <HeapIgnore> already applied.

In the below example, PinView is implemented with RedactedButton, a control that wraps Button. It accepts all the same arguments but has the ignore rules built in.

const RedactedButton = withHeapIgnore(Button, { allowInteraction: true });

const PinView = (props) => {
  return (
    <View>
      <RedactedButton title="1" rounded />
      <RedactedButton title="2" rounded />
      ...
    </View>
  );
};

Hiding Sensitive Data on Flutter

There are two options for hiding sensitive data on Flutter: Disabling text capture and configurable per-view capture rules.

Disabling all text capture

When initializing Heap, you have the option to disable text capture for all events that are automatically captured. To do this, set the disableInteractionTextCapture in your options object when calling Heap.startRecording. A similar property is available for disabling capture of all accessibility label text (shown below).

Heap().startRecording(
  'YOUR_ENVIRONMENT_ID',
  {
    'disableInteractionTextCapture': true,
    'disableInteractionAccessibilityLabelCapture': true,
  }
)

Redacting text for specific views

If you don't want to disable all text capture for your app, you can take a more targeted approach of only redacting text for specific views in your app that you know might contain sensitive data. When text is redacted for a view, it will still have target text, but the actual text will be replaced with ****. This can be done using the HeapRedact widget provided by Flutter autocapture.

@override
Widget build(BuildContext context) {
  return Center(
    child: Column(
      children: <Widget>[
        HeapRedact(
          redact: true,
          child: ElevatedButton( // This button is redacted
            onPressed: () {
              // Do something here
            },
            child: const Text('Button 1'),
          ),
        ),
        ElevatedButton(
          onPressed: () {
            // Do something here
          },
          child: const Text('Button 2'),
        ),
      ],
    ),
  ),
}
@override
Widget build(BuildContext context) {
  return Center(
    child: HeapRedact(
      redact: true,
      child: Column( // This whole column is redacted
        children: <Widget>[
          ElevatedButton( // This button is redacted
            onPressed: () {
              // Do something here
            },
            child: const Text('Button 1'),
          ),
          ElevatedButton( // This button is redacted
            onPressed: () {
              // Do something here
            },
            child: const Text('Button 2'),
          ),
        ],
      ),
    ),
  ),
}
@override
Widget build(BuildContext context) {
  return Center(
    child: HeapRedact(
      redact: true,
      child: Column( // This whole column is redacted
        children: <Widget>[
          HeapRedact(
            redact: false,
            child: ElevatedButton( // This button is NOT redacted
              onPressed: () {
                // Do something here
              },
              child: const Text('Button 1'),
            ),
          ),
          ElevatedButton( // This button is redacted
            onPressed: () {
              // Do something here
            },
            child: const Text('Button 2'),
          ),
        ],
      ),
    ),
  ),
}

Ignoring all interactions for specific views

If you find that certain interactions are prone to capturing sensitive data, or that a certain interaction might indicate sensitive information about a user during analysis, you have the option of ignoring all interactions on a per-widget basis. This can be done using the HeapIgnore widget provided by Flutter autocapture.

@override
Widget build(BuildContext context) {
  return Center(
    child: Column(
      children: <Widget>[
        HeapIgnore(
          ignore: true,
          child: ElevatedButton( // This button is ignored
            onPressed: () {
              // Do something here
            },
            child: const Text('Button 1'),
          ),
        ),
        ElevatedButton(
          onPressed: () {
            // Do something here
          },
          child: const Text('Button 2'),
        ),
      ],
    ),
  ),
}
@override
Widget build(BuildContext context) {
  return Center(
    child: HeapIgnore(
      ignore: true,
      child: Column( // This whole column is ignored
        children: <Widget>[
          ElevatedButton( // This button is ignored
            onPressed: () {
              // Do something here
            },
            child: const Text('Button 1'),
          ),
          ElevatedButton( // This button is ignored
            onPressed: () {
              // Do something here
            },
            child: const Text('Button 2'),
          ),
        ],
      ),
    ),
  ),
}
@override
Widget build(BuildContext context) {
  return Center(
    child: HeapIgnore(
      ignore: true,
      child: Column( // This whole column is ignored
        children: <Widget>[
          HeapIgnore(
            ignore: false,
            child: ElevatedButton( // This button is NOT ignored
              onPressed: () {
                // Do something here
              },
              child: const Text('Button 1'),
            ),
          ),
          ElevatedButton( // This button is ignored
            onPressed: () {
              // Do something here
            },
            child: const Text('Button 2'),
          ),
        ],
      ),
    ),
  ),
}