Audience: Data Science & Engineering Tracking System: Snowplow Structured Events (P2P module) Generated: 2026-02-14
All offer-related analytics are tracked via Snowplow structured events through the P2P module's KMP analytics layer. The main app (thredUP/) module does not fire any offer-specific events — tracking begins when the Make Offer bottom sheet component initializes.
Event structure: action | category | label | property | value
- User Flow Overview
- Buyer Make Offer Events
- Seller Offer Action Events
- Seller Counter Offer Events
- Price Breakdown Events
- MOP Page & Navigation Events
- MOP Offer List Interaction Events
- MOP Notification Settings Events
- Seller Listings Offer Events
- MOP Error Events
- Flow Sequence Diagram
- Notable Gaps
PDP (Offer Button Tap)
└─► Make Offer Modal ─── send-offer ───► Offer Created
│ │
└─ dismissed ▼
MOP (Messages & Offers Page)
├─ Buyer: Buy Now / Counter
└─ Seller: Accept / Counter
├─► Accept Flow (success/error)
└─► Counter Offer Flow (success/error)
Category: peer-to-peer-buyer-offer
Page: PeerToPeerDashboard
Source: BuyerOfferAnalytics.kt
| # | Label | Action | Property | Value | When It Fires |
|---|---|---|---|---|---|
| 1 | make-offer-opened |
modal-view |
item_number |
{itemNumber} |
Make Offer bottom sheet opens (from PDP button tap or buyer counter-offer) |
| 2 | suggestion-selected |
click |
index |
{index}:{priceInCents} |
User taps a suggested price chip |
| 3 | send-offer |
submit |
item_number |
{itemNumber}:{priceInCents} |
User taps "SEND OFFER" button |
| 4 | dismissed |
close |
— | — | User dismisses the sheet without submitting |
- PDP: "Offer" button in
ProductDetailsCTAs.kt→ opensMakeOfferBottomSheetHolder→MakeOfferComponent.initfiresmake-offer-opened - MOP Buyer Counter: From Messages & Offers Page when buyer counter-offers
| Field | Format | Example |
|---|---|---|
itemNumber |
String item ID | "123456" |
priceInCents |
Integer, cents | 1500 (= $15.00) |
index |
0-based suggestion position | 0, 1, 2 |
Category: peer-to-peer-seller-offer
Page: PeerToPeerDashboard
Source: SellerOfferAnalytics.kt
| # | Label | Action | Property | Value | When It Fires |
|---|---|---|---|---|---|
| 5 | accept-offer-clicked |
click |
offer_id |
{offerId}:{itemNumber} |
Seller taps "Accept" on an offer |
| 6 | accept-offer-success |
submit |
offer_id |
{offerId}:{priceInCents} |
Accept API returns success |
| 7 | accept-offer-error |
submit |
offer_id |
{offerId}:{error} |
Accept API returns error |
| Field | Format | Example |
|---|---|---|
offerId |
String offer ID | "offer_abc123" |
itemNumber |
String item ID | "123456" |
priceInCents |
Integer, cents | 1200 |
error |
Error message string | "network_error" |
Category: peer-to-peer-seller-offer
Page: PeerToPeerDashboard
Source: SellerOfferAnalytics.kt
| # | Label | Action | Property | Value | When It Fires |
|---|---|---|---|---|---|
| 8 | counter-offer-opened |
modal-view |
offer_id |
{offerId}:{itemNumber} |
Counter-offer sheet opens |
| 9 | counter-offer-dismissed |
close |
offer_id |
{offerId} |
Seller dismisses counter-offer sheet |
| 10 | counter-offer-price-entered |
click |
offer_id |
{offerId}:{priceInCents} |
Seller enters/changes price (declared but NOT called — see Gaps) |
| 11 | counter-offer-submitted |
submit |
offer_id |
{offerId}:{priceInCents} |
Seller taps submit |
| 12 | counter-offer-success |
submit |
offer_id |
{offerId}:{priceInCents} |
Counter-offer API returns success |
| 13 | counter-offer-error |
submit |
offer_id |
{offerId}:{error} |
Counter-offer API returns error |
Category: peer-to-peer-seller-offer
Page: PeerToPeerDashboard
Source: SellerOfferAnalytics.kt
| # | Label | Action | Property | Value | When It Fires |
|---|---|---|---|---|---|
| 14 | price-breakdown-viewed |
view-content |
item_number |
{itemNumber}:{priceInCents} |
Price breakdown data loaded successfully (counter-offer flow) |
| 15 | price-breakdown-error |
view-content |
item_number |
{itemNumber} |
Price breakdown API fails |
| 16 | price-breakdown-retry |
click |
item_number |
{itemNumber} |
Seller retries price breakdown load |
Category: peer-to-peer-mop
Page: PeerToPeerDashboard
Source: MopAnalytics.kt
| # | Label | Action | Property | Value | When It Fires |
|---|---|---|---|---|---|
| 17 | page-viewed |
view-content |
initial_tab |
{tab} (SHOP | SELL) |
MOP page loads |
| 18 | navigate-back |
click |
— | — | User taps back from MOP |
| 19 | tab-selected |
click |
tab |
{tab} (SHOP | SELL) |
User switches tabs |
| 20 | filter-selected |
click |
filter |
{filter} (OFFERS | MESSAGES) |
User taps filter chip |
Category: peer-to-peer-mop
Page: PeerToPeerDashboard
Source: MopAnalytics.kt
| # | Label | Action | Property | Value | When It Fires |
|---|---|---|---|---|---|
| 21 | offer-clicked |
click |
offer_id |
{offerId}:{itemNumber}:{tab} |
User taps an offer card |
| 22 | refresh |
page-reload |
tab |
{tab} |
User pulls to refresh |
| 23 | load-more |
click |
tab |
{tab}:{page} |
Infinite scroll loads next page |
| 24 | buy-now-clicked |
click |
offer_id |
{offerId}:{itemNumber} |
Buyer taps "Buy Now" on accepted offer |
| 25 | counter-clicked |
click |
offer_id |
{offerId}:{itemNumber}:{tab} |
User taps "Counter" on an offer |
| 26 | accept-clicked |
click |
offer_id |
{offerId}:{itemNumber} |
Seller taps "Accept" in MOP list |
When a seller accepts from MOP, two events fire:
accept-clicked(category:peer-to-peer-mop) — UI interaction in MOPaccept-offer-clicked(category:peer-to-peer-seller-offer) — business action
Same applies to counter: counter-clicked (MOP) + counter-offer-opened (seller-offer).
Category: peer-to-peer-mop
Page: PeerToPeerDashboard
Source: MopAnalytics.kt
| # | Label | Action | Property | Value | When It Fires |
|---|---|---|---|---|---|
| 27 | notifications-clicked |
click |
— | — | User taps bell icon |
| 28 | notification-sheet-opened |
modal-view |
— | — | Notification settings sheet opens |
| 29 | notification-sheet-dismissed |
close |
— | — | Notification sheet closed |
| 30 | email-toggled |
click |
enabled |
{true|false} |
User toggles email notifications |
| 31 | push-toggled |
click |
enabled |
{true|false} |
User toggles push notifications |
| 32 | notification-settings-saved |
submit |
— | — | User saves settings |
| 33 | view-all-notification-settings |
click |
— | — | User taps "View All" |
Source: SellerListingsAnalytics.kt
| # | Label | Action | Category | Property | Value | When It Fires |
|---|---|---|---|---|---|---|
| 34 | review-offer |
click |
peer-to-peer-listings |
item_id |
{itemId} |
Seller taps "Review Offer" on listing card |
| 35 | review-offer |
click |
peer-to-peer-listing-details |
— | — | Seller taps "Review Offer" on listing detail |
Category: peer-to-peer-mop
Source: MopAnalytics.kt
| # | Label | Action | Property | Value | When It Fires |
|---|---|---|---|---|---|
| 36 | error-dismissed |
close |
error_type |
{errorType} (e.g. "general") |
User dismisses error message |
User taps "Offer" on PDP
│
├─► [1] make-offer-opened (modal-view)
│
├─ User taps suggestion ─► [2] suggestion-selected (click)
│
├─ User taps "SEND OFFER" ─► [3] send-offer (submit)
│
└─ User dismisses ─► [4] dismissed (close)
User opens MOP
│
├─► [17] page-viewed (view-content)
│
├─ Switch tab ─► [19] tab-selected
├─ Switch filter ─► [20] filter-selected
├─ Tap offer card ─► [21] offer-clicked
├─ Pull refresh ─► [22] refresh
├─ Scroll more ─► [23] load-more
│
├─ Tap "Buy Now" ─► [24] buy-now-clicked
│
└─ Tap "Counter" ─► [25] counter-clicked
└─► [1] make-offer-opened (reuses buyer flow)
Seller taps "Accept" in MOP
│
├─► [26] accept-clicked (MOP event)
├─► [5] accept-offer-clicked (seller-offer event)
│
├─ API Success ─► [6] accept-offer-success
└─ API Error ─► [7] accept-offer-error
Seller taps "Counter" in MOP
│
├─► [25] counter-clicked (MOP event)
├─► [8] counter-offer-opened (seller-offer event)
│
├─ Price breakdown loads ─► [14] price-breakdown-viewed
├─ Price breakdown fails ─► [15] price-breakdown-error
│ └─ Retry ─► [16] price-breakdown-retry
│
├─ Submit ─► [11] counter-offer-submitted
│ ├─ Success ─► [12] counter-offer-success
│ └─ Error ─► [13] counter-offer-error
│
└─ Dismiss ─► [9] counter-offer-dismissed
| Gap | Description | Recommendation |
|---|---|---|
| No PDP offer button impression | No event fires when the "Offer" button is visible on PDP | Add offer-button-impression to measure visibility vs. conversion |
| No PDP offer button tap | Button tap is not separately tracked — only the sheet open | Consider adding offer-button-clicked for PDP funnel analysis |
counter-offer-price-entered is dead code |
Declared in SellerOfferAnalytics.kt but never called from CounterOfferComponent |
Either wire it up or remove it |
| No offer submission result on buyer side | send-offer fires on tap, but no send-offer-success / send-offer-error |
Add result events for buyer-side funnel completion |
| No custom price input tracking | No event when user types a custom price (only suggestion taps tracked) | Consider tracking custom-price-entered for price analysis |
| No offer expiry tracking | No event when an offer expires without action | Add server-side or client-side expiry events |
| File | Module | Content |
|---|---|---|
p2p/.../analytics/screen/BuyerOfferAnalytics.kt |
P2P | Buyer offer events (4 events) |
p2p/.../analytics/screen/SellerOfferAnalytics.kt |
P2P | Seller accept + counter + breakdown events (12 events) |
p2p/.../analytics/screen/MopAnalytics.kt |
P2P | MOP page, list, notifications, error events (18 events) |
p2p/.../analytics/screen/SellerListingsAnalytics.kt |
P2P | Seller listings review-offer events (2 events) |
p2p/.../feature/offer/MakeOfferComponent.kt |
P2P | Buyer offer component (fires events 1-4) |
p2p/.../feature/offer/CounterOfferComponent.kt |
P2P | Seller counter-offer component (fires events 9, 11-16) |
p2p/.../feature/mop/MessageOffersComponent.kt |
P2P | MOP component (fires events 5-8, 17-34) |
p2p/.../analytics/model/AnalyticsEvent.kt |
P2P | Event model definition |
p2p/.../analytics/tracker/SnowplowTracker.kt |
P2P | Snowplow bridge interface |
Total: 36 distinct analytics events across 4 analytics classes