xiaohongshu
xiaohongshu
fullstory-element-properties
Comprehensive guide for implementing Fullstory's Element Properties API across Web, Android, and iOS platforms. Teaches proper type handling, schema construction, value formatting, and platform-specific patterns including view/cell reuse. Includes detailed good/bad examples for e-commerce, forms,...
Full skill instructions
Fullstory's Element Properties API allows developers to capture custom properties on UI elements that can be used for search, filtering, grouping, and analytics. Unlike standard attributes which are only used for CSS selectors, element properties become first-class data points for analysis, similar to user properties, page properties, and event properties.
This skill covers implementation across Web (Browser), Android, and iOS platforms.
data-fs-element attribute with a namedata-fs-properties-schema can be set in the same hierarchyThis is one of the most powerful features of Element Properties:
Two-way inheritance:
┌─────────────────────────────────────────────────────────────────────┐
│ FORM (data-fs-element="checkout-form") │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SHIPPING SELECT (selectedShipping="express") │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ PAYMENT SELECT (selectedPayment="credit_card") │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ GIFT WRAP CHECKBOX (giftWrap="true") │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SUBMIT BUTTON (data-fs-element="submit-order") │ │
│ │ ─────────────────────────────────────────────────────── │ │
│ │ When clicked, captures ALL properties from siblings: │ │
│ │ • selectedShipping: "express" │ │
│ │ • selectedPayment: "credit_card" │ │
│ │ • giftWrap: true │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Why this matters for analytics:
<!-- Form with multiple selections and a submit button -->
<form data-fs-element="checkout-form">
<select data-selected-shipping data-fs-properties-schema='{"data-selected-shipping": "str"}'>
<option value="standard">Standard</option>
<option value="express" selected>Express</option>
</select>
<select data-selected-payment data-fs-properties-schema='{"data-selected-payment": "str"}'>
<option value="credit_card" selected>Credit Card</option>
<option value="paypal">PayPal</option>
</select>
<label>
<input
type="checkbox"
checked
data-gift-wrap="true"
data-fs-properties-schema='{"data-gift-wrap": "bool"}'
/>
Gift wrap
</label>
<!-- Submit button inherits ALL properties from above -->
<button type="submit" data-fs-element="submit-order">Place Order</button>
</form>
In Fullstory, when "submit-order" is clicked:
selectedShipping, selectedPayment, giftWrapKey Insight: Define element properties on individual form fields, but the submit button (or any parent action element) will automatically capture the state of all child properties at the moment of interaction. This eliminates the need to manually aggregate form state.
Element properties are defined using HTML data attributes:
data-fs-properties-schema: JSON object defining which attributes to capture and their typesdata-fs-element: (Optional) Names the element for use in Fullstory{
"attribute-name": "type",
"attribute-name-2": {
"type": "type_value",
"name": "override_property_name"
}
}
| Type | Description | Examples |
|---|---|---|
str | String value | "foo", "bar", "foo@bar" |
strs | Array of strings | ["foo", "bar"] |
int | Integer | 0, -123, 45 |
ints | Array of integers | [0, 1, 2] |
real | Float/decimal | 12.345, -0.5 |
reals | Array of reals | [12.345, 1] |
bool | Boolean | true, false, 1, 0, t, f |
bools | Array of booleans | [true, false] |
date | ISO8601 date/datetime | "2006-01-02T15:04:05Z" |
dates | Array of dates | ["2006-01-02", "2006-01-02T15:04:05Z"] |
<!-- GOOD: Comprehensive product card with proper typing and meaningful names -->
<div
class="product-card"
data-product-id="SKU-12345"
data-product-name="Premium Wireless Headphones"
data-product-category="Electronics"
data-product-price="199.99"
data-product-stock="15"
data-product-rating="4.5"
data-on-sale="true"
data-launch-date="2024-01-15"
data-tags='["wireless", "bluetooth", "noise-canceling"]'
data-fs-properties-schema='{
"data-product-id": {
"type": "str",
"name": "productId"
},
"data-product-name": {
"type": "str",
"name": "productName"
},
"data-product-category": {
"type": "str",
"name": "category"
},
"data-product-price": {
"type": "real",
"name": "price"
},
"data-product-stock": {
"type": "int",
"name": "stockLevel"
},
"data-product-rating": {
"type": "real",
"name": "rating"
},
"data-on-sale": {
"type": "bool",
"name": "isOnSale"
},
"data-launch-date": {
"type": "date",
"name": "launchDate"
},
"data-tags": {
"type": "strs",
"name": "productTags"
}
}'
data-fs-element="Product Card"
>
<button class="add-to-cart" data-fs-element="Add to Cart Button">Add to Cart</button>
</div>
Why this is good:
name override<!-- GOOD: Form-level properties inherited by all fields -->
<form
class="checkout-form"
data-form-type="checkout"
data-form-step="payment"
data-user-tier="premium"
data-fs-properties-schema='{
"data-form-type": {
"type": "str",
"name": "formType"
},
"data-form-step": {
"type": "str",
"name": "checkoutStep"
},
"data-user-tier": {
"type": "str",
"name": "userTier"
}
}'
data-fs-element="Checkout Form"
>
<input
type="text"
name="cardNumber"
data-field-name="cardNumber"
data-field-required="true"
data-field-validation="credit-card"
data-fs-properties-schema='{
"data-field-name": {
"type": "str",
"name": "fieldName"
},
"data-field-required": {
"type": "bool",
"name": "isRequired"
},
"data-field-validation": {
"type": "str",
"name": "validationType"
}
}'
data-fs-element="Card Number Field"
/>
<button
type="submit"
data-button-type="primary"
data-fs-properties-schema='{
"data-button-type": "str"
}'
data-fs-element="Submit Payment Button"
>
Complete Purchase
</button>
</form>
Why this is good:
// GOOD: Dynamically setting properties in a React component
function ProductCard({product}) {
const schemaRef = useRef(null)
useEffect(() => {
if (schemaRef.current) {
// Set the schema as a data attribute
schemaRef.current.setAttribute(
'data-fs-properties-schema',
JSON.stringify({
'data-product-id': {type: 'str', name: 'productId'},
'data-product-name': {type: 'str', name: 'productName'},
'data-price': {type: 'real', name: 'price'},
'data-in-stock': {type: 'bool', name: 'inStock'},
'data-variant-count': {type: 'int', name: 'variantCount'},
}),
)
// Set the actual attribute values
schemaRef.current.setAttribute('data-product-id', product.id)
schemaRef.current.setAttribute('data-product-name', product.name)
schemaRef.current.setAttribute('data-price', product.price.toString())
schemaRef.current.setAttribute('data-in-stock', product.inStock.toString())
schemaRef.current.setAttribute('data-variant-count', product.variants.length.toString())
schemaRef.current.setAttribute('data-fs-element', 'Product Card')
}
}, [product])
return (
<div ref={schemaRef} className="product-card">
{/* Component content */}
</div>
)
}
Why this is good:
<!-- BAD: Multiple type and naming issues -->
<button
class="product-buy"
data-price="$199.99"
data-quantity="5 items"
data-available="yes"
data-fs-properties-schema='{
"data-price": "str",
"data-quantity": "int",
"data-available": "bool"
}'
data-fs-element="Buy Button"
>
Buy Now
</button>
Why this is bad:
data-price contains "$" currency symbol but typed as string (should be real and clean numeric: "199.99")data-quantity contains "items" text but typed as int (should be clean number: "5")data-available uses "yes" instead of boolean value (should be "true" or "1")CORRECTED VERSION:
<!-- GOOD: Proper types and clean values -->
<button
class="product-buy"
data-price="199.99"
data-quantity="5"
data-available="true"
data-currency="USD"
data-fs-properties-schema='{
"data-price": {
"type": "real",
"name": "price"
},
"data-quantity": {
"type": "int",
"name": "quantity"
},
"data-available": {
"type": "bool",
"name": "isAvailable"
},
"data-currency": {
"type": "str",
"name": "currency"
}
}'
data-fs-element="Buy Button"
>
Buy Now
</button>
<!-- BAD: Capturing too many irrelevant properties -->
<div
data-component-name="ProductCard"
data-component-version="1.2.3"
data-react-key="product-123"
data-render-timestamp="2024-10-20T14:30:00Z"
data-css-class="product-card mb-4 shadow-lg"
data-developer-name="John Doe"
data-git-commit="abc123def456"
data-build-number="4567"
data-product-id="SKU-123"
data-product-name="Widget"
data-fs-properties-schema='{
"data-component-name": "str",
"data-component-version": "str",
"data-react-key": "str",
"data-render-timestamp": "date",
"data-css-class": "str",
"data-developer-name": "str",
"data-git-commit": "str",
"data-build-number": "int",
"data-product-id": "str",
"data-product-name": "str"
}'
></div>
Why this is bad:
CORRECTED VERSION:
<!-- GOOD: Only business-relevant properties -->
<div
data-product-id="SKU-123"
data-product-name="Widget"
data-product-category="Tools"
data-product-price="29.99"
data-fs-properties-schema='{
"data-product-id": {
"type": "str",
"name": "productId"
},
"data-product-name": {
"type": "str",
"name": "productName"
},
"data-product-category": {
"type": "str",
"name": "category"
},
"data-product-price": {
"type": "real",
"name": "price"
}
}'
data-fs-element="Product Card"
></div>
<!-- BAD: Multiple JSON and syntax errors -->
<button
data-product-id="SKU-123"
data-price="49.99"
data-fs-properties-schema="{
'data-product-id': 'str',
"data-price": "real"
}"
data-fs-element="Add to Cart">
Add to Cart
</button>
Why this is bad:
CORRECTED VERSION:
<!-- GOOD: Valid JSON with consistent quoting -->
<button
data-product-id="SKU-123"
data-price="49.99"
data-fs-properties-schema='{
"data-product-id": {
"type": "str",
"name": "productId"
},
"data-price": {
"type": "real",
"name": "price"
}
}'
data-fs-element="Add to Cart"
>
Add to Cart
</button>
<!-- BAD: Schema references attributes that don't exist -->
<button
data-product-id="SKU-123"
data-fs-properties-schema='{
"data-product-id": "str",
"data-product-name": "str",
"data-price": "real",
"data-category": "str"
}'
data-fs-element="Product Button"
>
View Product
</button>
Why this is bad:
data-product-name, data-price, data-category but these attributes don't existdata-product-id will be capturedCORRECTED VERSION:
<!-- GOOD: All referenced attributes exist -->
<button
data-product-id="SKU-123"
data-product-name="Premium Widget"
data-price="49.99"
data-category="Electronics"
data-fs-properties-schema='{
"data-product-id": {
"type": "str",
"name": "productId"
},
"data-product-name": {
"type": "str",
"name": "productName"
},
"data-price": {
"type": "real",
"name": "price"
},
"data-category": {
"type": "str",
"name": "category"
}
}'
data-fs-element="Product Button"
>
View Product
</button>
Element properties are set using the FS.setAttribute() method:
Java:
public static void FS.setAttribute(View view, String attributeName, String attributeValue);
Kotlin:
fun FS.setAttribute(view: View, attributeName: String, attributeValue: String)
// GOOD: Complete product card implementation with proper typing
public class ProductCardViewHolder extends RecyclerView.ViewHolder {
private View cardView;
public void bind(Product product) {
cardView = itemView.findViewById(R.id.product_card);
// Set individual attribute values
FS.setAttribute(cardView, "data-product-id", product.getId());
FS.setAttribute(cardView, "data-product-name", product.getName());
FS.setAttribute(cardView, "data-category", product.getCategory());
FS.setAttribute(cardView, "data-price", String.valueOf(product.getPrice()));
FS.setAttribute(cardView, "data-stock-level", String.valueOf(product.getStockLevel()));
FS.setAttribute(cardView, "data-rating", String.valueOf(product.getRating()));
FS.setAttribute(cardView, "data-on-sale", String.valueOf(product.isOnSale()));
FS.setAttribute(cardView, "data-launch-date", product.getLaunchDate().toString()); // ISO8601 format
// Set the properties schema - defines types for all attributes
String schema = new JSONObject()
.put("data-product-id", new JSONObject()
.put("type", "str")
.put("name", "productId"))
.put("data-product-name", new JSONObject()
.put("type", "str")
.put("name", "productName"))
.put("data-category", new JSONObject()
.put("type", "str")
.put("name", "category"))
.put("data-price", new JSONObject()
.put("type", "real")
.put("name", "price"))
.put("data-stock-level", new JSONObject()
.put("type", "int")
.put("name", "stockLevel"))
.put("data-rating", new JSONObject()
.put("type", "real")
.put("name", "rating"))
.put("data-on-sale", new JSONObject()
.put("type", "bool")
.put("name", "isOnSale"))
.put("data-launch-date", new JSONObject()
.put("type", "date")
.put("name", "launchDate"))
.toString();
FS.setAttribute(cardView, "data-fs-properties-schema", schema);
// Name the element for easy reference in Fullstory
FS.setAttribute(cardView, "data-fs-element", "Product Card");
// Configure child button - will inherit parent properties
Button addToCartBtn = itemView.findViewById(R.id.btn_add_to_cart);
FS.setAttribute(addToCartBtn, "data-fs-element", "Add to Cart Button");
}
}
Why this is good:
name field// GOOD: Comprehensive form tracking with hierarchy
class CheckoutFormFragment : Fragment() {
private fun setupFormTracking() {
val formContainer = view?.findViewById<ViewGroup>(R.id.checkout_form)
// Set form-level properties that all fields will inherit
formContainer?.let { form ->
FS.setAttribute(form, "data-form-type", "checkout")
FS.setAttribute(form, "data-form-step", "payment")
FS.setAttribute(form, "data-user-tier", "premium")
FS.setAttribute(form, "data-total-amount", viewModel.cartTotal.toString())
val formSchema = JSONObject().apply {
put("data-form-type", JSONObject().put("type", "str").put("name", "formType"))
put("data-form-step", JSONObject().put("type", "str").put("name", "checkoutStep"))
put("data-user-tier", JSONObject().put("type", "str").put("name", "userTier"))
put("data-total-amount", JSONObject().put("type", "real").put("name", "cartTotal"))
}.toString()
FS.setAttribute(form, "data-fs-properties-schema", formSchema)
FS.setAttribute(form, "data-fs-element", "Checkout Form")
}
// Set field-specific properties
val cardNumberField = view?.findViewById<EditText>(R.id.card_number_field)
cardNumberField?.let { field ->
FS.setAttribute(field, "data-field-name", "cardNumber")
FS.setAttribute(field, "data-field-required", "true")
FS.setAttribute(field, "data-field-type", "credit-card")
FS.setAttribute(field, "data-max-length", "16")
val fieldSchema = JSONObject().apply {
put("data-field-name", JSONObject().put("type", "str").put("name", "fieldName"))
put("data-field-required", JSONObject().put("type", "bool").put("name", "isRequired"))
put("data-field-type", JSONObject().put("type", "str").put("name", "fieldType"))
put("data-max-length", JSONObject().put("type", "int").put("name", "maxLength"))
}.toString()
FS.setAttribute(field, "data-fs-properties-schema", fieldSchema)
FS.setAttribute(field, "data-fs-element", "Card Number Field")
}
// Submit button inherits form properties
val submitButton = view?.findViewById<Button>(R.id.submit_payment)
submitButton?.let { button ->
FS.setAttribute(button, "data-button-type", "primary")
FS.setAttribute(button, "data-button-action", "submit-payment")
val buttonSchema = JSONObject().apply {
put("data-button-type", "str")
put("data-button-action", JSONObject().put("type", "str").put("name", "buttonAction"))
}.toString()
FS.setAttribute(button, "data-fs-properties-schema", buttonSchema)
FS.setAttribute(button, "data-fs-element", "Submit Payment Button")
}
}
}
Why this is good:
// GOOD: Handling dynamic list items with proper cleanup
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ViewHolder> {
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Product product = products.get(position);
// Clear previous attributes (important for recycled views)
holder.clearFullstoryAttributes();
// Set new attributes
holder.setProductProperties(product);
}
static class ViewHolder extends RecyclerView.ViewHolder {
private List<String> appliedAttributes = new ArrayList<>();
void setProductProperties(Product product) {
View itemView = this.itemView;
// Track which attributes we're setting
appliedAttributes.clear();
// Set attributes
setAttribute(itemView, "data-product-id", product.getId());
setAttribute(itemView, "data-product-name", product.getName());
setAttribute(itemView, "data-price", String.valueOf(product.getPrice()));
setAttribute(itemView, "data-in-stock", String.valueOf(product.isInStock()));
setAttribute(itemView, "data-position", String.valueOf(getAdapterPosition()));
// Build and set schema
try {
String schema = new JSONObject()
.put("data-product-id", new JSONObject().put("type", "str").put("name", "productId"))
.put("data-product-name", new JSONObject().put("type", "str").put("name", "productName"))
.put("data-price", new JSONObject().put("type", "real").put("name", "price"))
.put("data-in-stock", new JSONObject().put("type", "bool").put("name", "inStock"))
.put("data-position", new JSONObject().put("type", "int").put("name", "listPosition"))
.toString();
setAttribute(itemView, "data-fs-properties-schema", schema);
setAttribute(itemView, "data-fs-element", "Product List Item");
} catch (JSONException e) {
Log.e("ProductAdapter", "Error building properties schema", e);
}
}
private void setAttribute(View view, String name, String value) {
FS.setAttribute(view, name, value);
appliedAttributes.add(name);
}
void clearFullstoryAttributes() {
// Note: There's no official remove method, but setting empty values works
for (String attr : appliedAttributes) {
FS.setAttribute(itemView, attr, "");
}
appliedAttributes.clear();
}
}
}
Why this is good:
// BAD: Multiple type and formatting issues
public void bindProductCard(Product product) {
View cardView = findViewById(R.id.product_card);
// Setting formatted values instead of clean data
FS.setAttribute(cardView, "data-price", "$" + product.getPrice()); // BAD: includes $
FS.setAttribute(cardView, "data-stock", product.getStockLevel() + " items"); // BAD: includes "items"
FS.setAttribute(cardView, "data-available", product.isInStock() ? "Yes" : "No"); // BAD: uses Yes/No not bool
FS.setAttribute(cardView, "data-rating", product.getRating() + "/5 stars"); // BAD: includes text
// Schema with wrong types
String schema = "{" +
"\"data-price\": \"str\"," + // BAD: should be "real"
"\"data-stock\": \"int\"," + // BAD: value has text, will fail
"\"data-available\": \"bool\"," + // BAD: value is "Yes"/"No" not boolean
"\"data-rating\": \"str\"" + // BAD: should be "real"
"}";
FS.setAttribute(cardView, "data-fs-properties-schema", schema);
FS.setAttribute(cardView, "data-fs-element", "Product Card");
}
Why this is bad:
CORRECTED VERSION:
// GOOD: Clean values with proper types
public void bindProductCard(Product product) {
View cardView = findViewById(R.id.product_card);
// Set clean values
FS.setAttribute(cardView, "data-price", String.valueOf(product.getPrice()));
FS.setAttribute(cardView, "data-stock", String.valueOf(product.getStockLevel()));
FS.setAttribute(cardView, "data-available", String.valueOf(product.isInStock()));
FS.setAttribute(cardView, "data-rating", String.valueOf(product.getRating()));
FS.setAttribute(cardView, "data-currency", "USD"); // Separate attribute for currency
try {
String schema = new JSONObject()
.put("data-price", new JSONObject()
.put("type", "real")
.put("name", "price"))
.put("data-stock", new JSONObject()
.put("type", "int")
.put("name", "stockLevel"))
.put("data-available", new JSONObject()
.put("type", "bool")
.put("name", "isAvailable"))
.put("data-rating", new JSONObject()
.put("type", "real")
.put("name", "rating"))
.put("data-currency", new JSONObject()
.put("type", "str")
.put("name", "currency"))
.toString();
FS.setAttribute(cardView, "data-fs-properties-schema", schema);
FS.setAttribute(cardView, "data-fs-element", "Product Card");
} catch (JSONException e) {
Log.e("ProductCard", "Error building schema", e);
}
}
// BAD: Setting schema before attribute values
fun setupProductCard(product: Product) {
val cardView = findViewById<View>(R.id.product_card)
// BAD: Setting schema first
val schema = JSONObject().apply {
put("data-product-id", "str")
put("data-price", "real")
}.toString()
FS.setAttribute(cardView, "data-fs-properties-schema", schema)
// Then setting values (TOO LATE - schema won't capture these properly)
FS.setAttribute(cardView, "data-product-id", product.id)
FS.setAttribute(cardView, "data-price", product.price.toString())
FS.setAttribute(cardView, "data-fs-element", "Product Card")
}
Why this is bad:
CORRECTED VERSION:
// GOOD: Setting values first, then schema
fun setupProductCard(product: Product) {
val cardView = findViewById<View>(R.id.product_card)
// Set attribute values FIRST
FS.setAttribute(cardView, "data-product-id", product.id)
FS.setAttribute(cardView, "data-price", product.price.toString())
// Then set schema
val schema = JSONObject().apply {
put("data-product-id", JSONObject().put("type", "str").put("name", "productId"))
put("data-price", JSONObject().put("type", "real").put("name", "price"))
}.toString()
FS.setAttribute(cardView, "data-fs-properties-schema", schema)
// Finally name the element
FS.setAttribute(cardView, "data-fs-element", "Product Card")
}
// BAD: Not converting primitive types to String
public void setupView(View view, int quantity, double price, boolean available) {
// These will cause compilation errors
FS.setAttribute(view, "data-quantity", quantity); // ERROR: expects String
FS.setAttribute(view, "data-price", price); // ERROR: expects String
FS.setAttribute(view, "data-available", available); // ERROR: expects String
}
Why this is bad:
FS.setAttribute() expects String for all valuesCORRECTED VERSION:
// GOOD: Proper type conversion to String
public void setupView(View view, int quantity, double price, boolean available) {
FS.setAttribute(view, "data-quantity", String.valueOf(quantity));
FS.setAttribute(view, "data-price", String.valueOf(price));
FS.setAttribute(view, "data-available", String.valueOf(available));
try {
String schema = new JSONObject()
.put("data-quantity", new JSONObject().put("type", "int").put("name", "quantity"))
.put("data-price", new JSONObject().put("type", "real").put("name", "price"))
.put("data-available", new JSONObject().put("type", "bool").put("name", "isAvailable"))
.toString();
FS.setAttribute(view, "data-fs-properties-schema", schema);
} catch (JSONException e) {
Log.e("ViewSetup", "Error building schema", e);
}
}
// BAD: Not handling view recycling properly
class ProductAdapter : RecyclerView.Adapter<ProductAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val product = products[position]
// BAD: Setting attributes without clearing old ones from recycled view
FS.setAttribute(holder.itemView, "data-product-id", product.id)
FS.setAttribute(holder.itemView, "data-product-name", product.name)
// More attributes...
// If this view was recycled, it might still have old attributes
// from previous product that no longer apply
}
}
Why this is bad:
CORRECTED VERSION:
// GOOD: Properly handling recycled views
class ProductAdapter : RecyclerView.Adapter<ProductAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val product = products[position]
// Clear old attributes by setting empty values
// (Note: Track previously set attributes in ViewHolder)
holder.clearPreviousAttributes()
// Set new attributes
holder.setProductAttributes(product, position)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val previousAttributes = mutableListOf<String>()
fun clearPreviousAttributes() {
previousAttributes.forEach { attr ->
FS.setAttribute(itemView, attr, "")
}
previousAttributes.clear()
}
fun setProductAttributes(product: Product, position: Int) {
// Set and track attributes
setAndTrack("data-product-id", product.id)
setAndTrack("data-product-name", product.name)
setAndTrack("data-price", product.price.toString())
setAndTrack("data-position", position.toString())
// Set schema
val schema = JSONObject().apply {
put("data-product-id", JSONObject().put("type", "str").put("name", "productId"))
put("data-product-name", JSONObject().put("type", "str").put("name", "productName"))
put("data-price", JSONObject().put("type", "real").put("name", "price"))
put("data-position", JSONObject().put("type", "int").put("name", "listPosition"))
}.toString()
setAndTrack("data-fs-properties-schema", schema)
setAndTrack("data-fs-element", "Product List Item")
}
private fun setAndTrack(attr: String, value: String) {
FS.setAttribute(itemView, attr, value)
previousAttributes.add(attr)
}
}
}
Element properties are set using the FS.setAttribute() method:
Objective-C:
[FS setAttribute:view attributeName:@"attribute-name" attributeValue:@"value"];
Swift:
FS.setAttribute(view: UIView, attributeName: String, attributeValue: String)
// GOOD: Complete product card implementation
class ProductCardView: UIView {
private var addToCartButton: UIButton!
func configure(with product: Product) {
// Set all attribute values first
FS.setAttribute(self, attributeName: "data-product-id", attributeValue: product.id)
FS.setAttribute(self, attributeName: "data-product-name", attributeValue: product.name)
FS.setAttribute(self, attributeName: "data-category", attributeValue: product.category)
FS.setAttribute(self, attributeName: "data-price", attributeValue: String(product.price))
FS.setAttribute(self, attributeName: "data-stock-level", attributeValue: String(product.stockLevel))
FS.setAttribute(self, attributeName: "data-rating", attributeValue: String(product.rating))
FS.setAttribute(self, attributeName: "data-on-sale", attributeValue: String(product.isOnSale))
FS.setAttribute(self, attributeName: "data-launch-date", attributeValue: product.launchDate.iso8601String)
// Build properties schema as JSON string
let schema: [String: Any] = [
"data-product-id": [
"type": "str",
"name": "productId"
],
"data-product-name": [
"type": "str",
"name": "productName"
],
"data-category": [
"type": "str",
"name": "category"
],
"data-price": [
"type": "real",
"name": "price"
],
"data-stock-level": [
"type": "int",
"name": "stockLevel"
],
"data-rating": [
"type": "real",
"name": "rating"
],
"data-on-sale": [
"type": "bool",
"name": "isOnSale"
],
"data-launch-date": [
"type": "date",
"name": "launchDate"
]
]
// Convert to JSON string
if let schemaData = try? JSONSerialization.data(withJSONObject: schema),
let schemaString = String(data: schemaData, encoding: .utf8) {
FS.setAttribute(self, attributeName: "data-fs-properties-schema", attributeValue: schemaString)
}
// Name the element
FS.setAttribute(self, attributeName: "data-fs-element", attributeValue: "Product Card")
// Configure child button (inherits parent properties)
FS.setAttribute(addToCartButton, attributeName: "data-fs-element", attributeValue: "Add to Cart Button")
}
}
// Helper extension for ISO8601 dates
extension Date {
var iso8601String: String {
let formatter = ISO8601DateFormatter()
return formatter.string(from: self)
}
}
Why this is good:
name field// GOOD: Properly handling cell reuse in UITableView
class ProductTableViewCell: UITableViewCell {
private var currentAttributes: [String] = []
override func prepareForReuse() {
super.prepareForReuse()
clearFullstoryAttributes()
}
func configure(with product: Product, at indexPath: IndexPath) {
// Clear any existing attributes first
clearFullstoryAttributes()
// Set new attributes and track them
setAndTrack("data-product-id", product.id)
setAndTrack("data-product-name", product.name)
setAndTrack("data-price", String(product.price))
setAndTrack("data-in-stock", String(product.isInStock))
setAndTrack("data-row", String(indexPath.row))
setAndTrack("data-section", String(indexPath.section))
// Build schema
let schema: [String: Any] = [
"data-product-id": ["type": "str", "name": "productId"],
"data-product-name": ["type": "str", "name": "productName"],
"data-price": ["type": "real", "name": "price"],
"data-in-stock": ["type": "bool", "name": "inStock"],
"data-row": ["type": "int", "name": "rowIndex"],
"data-section": ["type": "int", "name": "sectionIndex"]
]
if let schemaData = try? JSONSerialization.data(withJSONObject: schema),
let schemaString = String(data: schemaData, encoding: .utf8) {
setAndTrack("data-fs-properties-schema", schemaString)
}
setAndTrack("data-fs-element", "Product Cell")
}
private func setAndTrack(_ attribute: String, _ value: String) {
FS.setAttribute(self, attributeName: attribute, attributeValue: value)
currentAttributes.append(attribute)
}
private func clearFullstoryAttributes() {
currentAttributes.forEach { attribute in
FS.setAttribute(self, attributeName: attribute, attributeValue: "")
}
currentAttributes.removeAll()
}
}
Why this is good:
prepareForReuse()// GOOD: Form implementation with property inheritance
@implementation CheckoutFormViewController
- (void)setupFormTracking {
UIView *formContainer = self.formContainerView;
// Set form-level properties (inherited by all children)
[FS setAttribute:formContainer
attributeName:@"data-form-type"
attributeValue:@"checkout"];
[FS setAttribute:formContainer
attributeName:@"data-form-step"
attributeValue:@"payment"];
[FS setAttribute:formContainer
attributeName:@"data-user-tier"
attributeValue:self.userTier];
[FS setAttribute:formContainer
attributeName:@"data-cart-total"
attributeValue:[NSString stringWithFormat:@"%.2f", self.cartTotal]];
// Build form schema
NSDictionary *formSchema = @{
@"data-form-type": @{
@"type": @"str",
@"name": @"formType"
},
@"data-form-step": @{
@"type": @"str",
@"name": @"checkoutStep"
},
@"data-user-tier": @{
@"type": @"str",
@"name": @"userTier"
},
@"data-cart-total": @{
@"type": @"real",
@"name": @"cartTotal"
}
};
NSError *error;
NSData *formSchemaData = [NSJSONSerialization dataWithJSONObject:formSchema
options:0
error:&error];
if (formSchemaData && !error) {
NSString *formSchemaString = [[NSString alloc] initWithData:formSchemaData
encoding:NSUTF8StringEncoding];
[FS setAttribute:formContainer
attributeName:@"data-fs-properties-schema"
attributeValue:formSchemaString];
}
[FS setAttribute:formContainer
attributeName:@"data-fs-element"
attributeValue:@"Checkout Form"];
// Set field-specific properties
[self setupFieldTracking:self.cardNumberField
fieldName:@"cardNumber"
required:YES
fieldType:@"credit-card"];
// Submit button inherits form properties
[FS setAttribute:self.submitButton
attributeName:@"data-button-type"
attributeValue:@"primary"];
[FS setAttribute:self.submitButton
attributeName:@"data-fs-element"
attributeValue:@"Submit Payment Button"];
}
- (void)setupFieldTracking:(UITextField *)field
fieldName:(NSString *)name
required:(BOOL)required
fieldType:(NSString *)type {
[FS setAttribute:field attributeName:@"data-field-name" attributeValue:name];
[FS setAttribute:field attributeName:@"data-field-required"
attributeValue:required ? @"true" : @"false"];
[FS setAttribute:field attributeName:@"data-field-type" attributeValue:type];
NSDictionary *fieldSchema = @{
@"data-field-name": @{@"type": @"str", @"name": @"fieldName"},
@"data-field-required": @{@"type": @"bool", @"name": @"isRequired"},
@"data-field-type": @{@"type": @"str", @"name": @"fieldType"}
};
NSData *schemaData = [NSJSONSerialization dataWithJSONObject:fieldSchema
options:0
error:nil];
if (schemaData) {
NSString *schemaString = [[NSString alloc] initWithData:schemaData
encoding:NSUTF8StringEncoding];
[FS setAttribute:field
attributeName:@"data-fs-properties-schema"
attributeValue:schemaString];
}
[FS setAttribute:field
attributeName:@"data-fs-element"
attributeValue:[NSString stringWithFormat:@"%@ Field", name]];
}
@end
Why this is good:
// BAD: Multiple type and formatting problems
func configureProductCard(with product: Product) {
let cardView = self.productCardView
// BAD: Including formatting/symbols in numeric values
FS.setAttribute(cardView,
attributeName: "data-price",
attributeValue: "$\(product.price)") // BAD: includes $
FS.setAttribute(cardView,
attributeName: "data-stock",
attributeValue: "\(product.stockLevel) items") // BAD: includes "items"
FS.setAttribute(cardView,
attributeName: "data-available",
attributeValue: product.isInStock ? "Yes" : "No") // BAD: not boolean value
FS.setAttribute(cardView,
attributeName: "data-rating",
attributeValue: "\(product.rating) stars") // BAD: includes "stars"
// Schema with wrong types
let schema = """
{
"data-price": "str",
"data-stock": "int",
"data-available": "bool",
"data-rating": "str"
}
"""
FS.setAttribute(cardView,
attributeName: "data-fs-properties-schema",
attributeValue: schema)
}
Why this is bad:
CORRECTED VERSION:
// GOOD: Clean values with proper types
func configureProductCard(with product: Product) {
let cardView = self.productCardView
// Set clean numeric and boolean values
FS.setAttribute(cardView, attributeName: "data-price",
attributeValue: String(product.price))
FS.setAttribute(cardView, attributeName: "data-stock",
attributeValue: String(product.stockLevel))
FS.setAttribute(cardView, attributeName: "data-available",
attributeValue: String(product.isInStock))
FS.setAttribute(cardView, attributeName: "data-rating",
attributeValue: String(product.rating))
FS.setAttribute(cardView, attributeName: "data-currency",
attributeValue: "USD") // Separate field for currency
// Build schema using Dictionary for safety
let schema: [String: Any] = [
"data-price": ["type": "real", "name": "price"],
"data-stock": ["type": "int", "name": "stockLevel"],
"data-available": ["type": "bool", "name": "isAvailable"],
"data-rating": ["type": "real", "name": "rating"],
"data-currency": ["type": "str", "name": "currency"]
]
if let schemaData = try? JSONSerialization.data(withJSONObject: schema),
let schemaString = String(data: schemaData, encoding: .utf8) {
FS.setAttribute(cardView,
attributeName: "data-fs-properties-schema",
attributeValue: schemaString)
}
FS.setAttribute(cardView,
attributeName: "data-fs-element",
attributeValue: "Product Card")
}
// BAD: UITableViewCell without cleaning up attributes
class ProductCell: UITableViewCell {
func configure(with product: Product) {
// BAD: Just setting new attributes without clearing old ones
FS.setAttribute(self, attributeName: "data-product-id", attributeValue: product.id)
FS.setAttribute(self, attributeName: "data-product-name", attributeValue: product.name)
// When this cell is reused, old attributes from previous product remain!
// This can cause wrong data to be captured
}
}
Why this is bad:
prepareForReuse()CORRECTED VERSION:
// GOOD: Proper cell reuse handling
class ProductCell: UITableViewCell {
private var trackedAttributes: [String] = []
override func prepareForReuse() {
super.prepareForReuse()
clearAttributes()
}
func configure(with product: Product) {
clearAttributes()
setAndTrack("data-product-id", product.id)
setAndTrack("data-product-name", product.name)
setAndTrack("data-price", String(product.price))
let schema: [String: Any] = [
"data-product-id": ["type": "str", "name": "productId"],
"data-product-name": ["type": "str", "name": "productName"],
"data-price": ["type": "real", "name": "price"]
]
if let schemaData = try? JSONSerialization.data(withJSONObject: schema),
let schemaString = String(data: schemaData, encoding: .utf8) {
setAndTrack("data-fs-properties-schema", schemaString)
}
setAndTrack("data-fs-element", "Product Cell")
}
private func setAndTrack(_ attr: String, _ value: String) {
FS.setAttribute(self, attributeName: attr, attributeValue: value)
trackedAttributes.append(attr)
}
private func clearAttributes() {
trackedAttributes.forEach { attr in
FS.setAttribute(self, attributeName: attr, attributeValue: "")
}
trackedAttributes.removeAll()
}
}
// BAD: Setting schema before values
- (void)configureProductCard:(Product *)product {
UIView *cardView = self.productCardView;
// BAD: Setting schema first
NSDictionary *schema = @{
@"data-product-id": @"str",
@"data-price": @"real"
};
NSData *schemaData = [NSJSONSerialization dataWithJSONObject:schema
options:0
error:nil];
NSString *schemaString = [[NSString alloc] initWithData:schemaData
encoding:NSUTF8StringEncoding];
[FS setAttribute:cardView
attributeName:@"data-fs-properties-schema"
attributeValue:schemaString];
// Then setting values (wrong order)
[FS setAttribute:cardView
attributeName:@"data-product-id"
attributeValue:product.productId];
[FS setAttribute:cardView
attributeName:@"data-price"
attributeValue:[NSString stringWithFormat:@"%.2f", product.price]];
}
Why this is bad:
CORRECTED VERSION:
// GOOD: Values first, then schema
- (void)configureProductCard:(Product *)product {
UIView *cardView = self.productCardView;
// Set values FIRST
[FS setAttribute:cardView
attributeName:@"data-product-id"
attributeValue:product.productId];
[FS setAttribute:cardView
attributeName:@"data-price"
attributeValue:[NSString stringWithFormat:@"%.2f", product.price]];
// Then set schema
NSDictionary *schema = @{
@"data-product-id": @{@"type": @"str", @"name": @"productId"},
@"data-price": @{@"type": @"real", @"name": @"price"}
};
NSData *schemaData = [NSJSONSerialization dataWithJSONObject:schema
options:0
error:nil];
if (schemaData) {
NSString *schemaString = [[NSString alloc] initWithData:schemaData
encoding:NSUTF8StringEncoding];
[FS setAttribute:cardView
attributeName:@"data-fs-properties-schema"
attributeValue:schemaString];
}
[FS setAttribute:cardView
attributeName:@"data-fs-element"
attributeValue:@"Product Card"];
}
// BAD: Manually building JSON strings
func setupProduct(_ product: Product) {
let cardView = self.cardView
FS.setAttribute(cardView, attributeName: "data-price", attributeValue: String(product.price))
FS.setAttribute(cardView, attributeName: "data-name", attributeValue: product.name)
// BAD: Manual JSON string construction
let schema = """
{
"data-price": {
"type": "real",
"name": "price"
},
"data-name": {
"type": "str",
"name": "productName"
}
}
"""
FS.setAttribute(cardView, attributeName: "data-fs-properties-schema", attributeValue: schema)
}
Why this is bad:
CORRECTED VERSION:
// GOOD: Using Dictionary and JSONSerialization
func setupProduct(_ product: Product) {
let cardView = self.cardView
FS.setAttribute(cardView, attributeName: "data-price",
attributeValue: String(product.price))
FS.setAttribute(cardView, attributeName: "data-name",
attributeValue: product.name)
// Use Dictionary for type safety
let schema: [String: Any] = [
"data-price": [
"type": "real",
"name": "price"
],
"data-name": [
"type": "str",
"name": "productName"
]
]
// Convert to JSON safely
if let schemaData = try? JSONSerialization.data(withJSONObject: schema),
let schemaString = String(data: schemaData, encoding: .utf8) {
FS.setAttribute(cardView,
attributeName: "data-fs-properties-schema",
attributeValue: schemaString)
}
}
Container (e.g., product card)
├── Properties: productId, productName, price, category
└── Button (e.g., "Add to Cart")
└── Inherits all container properties
└── Button interaction captures full product context
Form Container
├── Properties: formType, formStep, userTier
├── Field 1
│ ├── Inherits form properties
│ └── Own properties: fieldName, isRequired, fieldType
├── Field 2
│ └── Same pattern
└── Submit Button
└── Inherits form properties
List Container
├── Properties: listType, totalItems, filterApplied
└── List Item (repeated)
├── Inherits list properties
└── Own properties: itemId, itemName, position
name overridedata-fs-elementSymptom: Properties don't show up in Fullstory after element interaction
Common Causes:
Solutions:
Symptom: Properties show incorrect or stale values
Common Causes (Mobile):
Solutions:
Symptom: Numeric/boolean properties show as strings
Common Causes:
Solutions:
When helping developers implement Element Properties:
Always emphasize:
name overrideFor Web: Remind about JSON validity and proper data attribute syntax
For Android: Emphasize String.valueOf() conversions and RecyclerView reuse handling
For iOS: Emphasize JSONSerialization over manual strings and UITableView/UICollectionView reuse
Always ask about:
Red flags to watch for:
This skill document was created to help Agent understand and guide developers in implementing Fullstory's Element Properties API correctly across Web and Mobile platforms.
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience