Skip to content

Sobre o Braze JavaScript SDK

O Braze JavaScript SDK ajuda você a integrar recursos de envio de mensagens, análise de dados e engajamento de usuários da Braze ao seu aplicativo.

Para começar, consulte os seguintes recursos:

Visão geral da arquitetura

O Braze JavaScript SDK é uma biblioteca independente de plataforma projetada para funcionar em qualquer ambiente JavaScript puro. Ele não contém APIs específicas de navegador ou Node.js, o que o torna adequado para uso em diversos runtimes JavaScript.

Princípios de design principais:

  • Injeção de dependência: O SDK requer implementações para armazenamento, rede e informações do dispositivo em vez de usar APIs específicas de plataforma
  • Async-First: A maioria dos métodos da API é assíncrona e retorna Promises; alguns métodos utilitários (por exemplo, destroy, subscribeToInAppMessage, toggleLogging, setLogger) são síncronos. Consulte as definições TypeScript para assinaturas exatas.
  • Sessão singleton: A API em nível de módulo (initialize/destroy) gerencia uma sessão ativa do SDK por vez.
  • Gerenciamento interno de dependências: Cria e gerencia dependências internas (UserManager, SessionManager, DataFlushController, etc.) a partir das implementações fornecidas

Início rápido

Instale o SDK com npm:

1
npm install @braze/javascript-sdk

Ou com yarn:

1
yarn add @braze/javascript-sdk

Com a API em nível de módulo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { initialize, openSession, changeUser } from '@braze/javascript-sdk';

await initialize({
  apiKey,
  baseUrl,
  options,
  sdkMetadata,
  deviceInfo,
  storageManager,
  networkManager,
});

await changeUser(userId);

await openSession();

Pré-requisitos

Antes de integrar o Braze JavaScript SDK, você precisará de:

  • Conta na Braze: Uma conta na Braze com acesso à API
  • Chave de API: A chave de API do seu app no dashboard da Braze
  • Endpoint do SDK: A URL do endpoint do SDK da Braze (por exemplo, sdk.iad-01.braze.com)

Obtendo suas credenciais

  1. Chave de API: Encontrada no dashboard da Braze em ConfiguraçõesChaves de API
  2. Endpoint do SDK: Localizado em ConfiguraçõesAutenticação do SDKEndpoints

Integração

Chamando a API

Use a API em nível de módulo: chame initialize() uma vez e depois chame as funções exportadas. Para trocar a configuração, chame destroy() primeiro e depois initialize() novamente.

1
2
3
4
5
import { initialize, logPurchase, changeUser } from '@braze/javascript-sdk';

await initialize({ apiKey, baseUrl, options, ... });
await changeUser('user-123');
await logPurchase('sku-1', 9.99, 'USD', 1);

Conceitos principais

Implementações obrigatórias

O objeto de configuração do initialize requer storageManager. networkManager e pushManager são opcionais.

1. StorageManager - Interface de armazenamento assíncrono chave-valor

1
2
3
4
5
6
interface StorageManager {
  store(key: string, value: string, isId?: boolean): Promise<void>;
  remove(key: string, isId?: boolean): Promise<void>;
  retrieve(key: string, isId?: boolean): Promise<string | null>;
  clearData(storageKeys: string[]): Promise<void>;
}
  • O parâmetro isId indica armazenamento persistente de ID: quando true, o SDK está armazenando um identificador persistente (ID do dispositivo, ID do usuário) ou a flag de opt-out. As implementações devem persistir esses dados entre reinicializações do app para que o SDK possa reconhecer o mesmo dispositivo/usuário. Quando false, o valor é dado de sessão/cache (eventos, atributos, etc.) e pode ficar apenas em memória. Para ambientes web, considere usar cookies para chaves armazenadas com isId: true para garantir persistência entre sessões.
  • Deve lidar com operações assíncronas para todas as operações de armazenamento

2. NetworkManager (opcional) - Interface de requisição HTTP POST

1
2
3
4
5
6
7
interface NetworkManager {
  postRequest(
    url: string,
    data: Partial<Record<string, unknown>>,
    headers?: globalThis.Headers | [string, string][]
  ): Promise<Partial<Record<string, unknown>>>;
}
  • A implementação padrão usa a API fetch (requer fetch e URL globais)
  • Pode ser substituída se fetch não for a API preferida
  • Nota: O SDK já possui lógica de retry e limite de taxa integrada

3. PushManager (opcional) - Interface de notificação por push

1
2
3
4
5
6
7
8
9
10
interface PushManager {
  isPushBlocked(): boolean | undefined;
  isPushPermissionGranted(): boolean | undefined;
  isPushSupported(): boolean | undefined;
  registerPush(
    successCallback?: (endpoint: string, publicKey: string, userAuth: string) => void,
    deniedCallback?: (temporaryDenial: boolean) => void,
  ): void;
  unregisterPush(successCallback?: () => void, errorCallback?: () => void): void;
}
  • Necessário apenas se você estiver implementando notificações por push

Envio de dados (Data Flushing)

O SDK envia automaticamente os dados em cache para os servidores da Braze a cada 10 segundos (configurável via flushIntervalInSeconds). Use requestImmediateDataFlush() para forçar a sincronização imediata.

Padrões de integração

Para assinaturas de métodos, tipos de parâmetros e retorno, e detalhes completos da API, consulte as definições TypeScript no pacote.

Integração básica

Exemplo funcional completo com tratamento de erros:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import {
  initialize,
  openSession,
  changeUser,
  logCustomEvent,
  type StorageManager,
  type DeviceInfo
} from '@braze/javascript-sdk';

// Implement required StorageManager interface (in-memory only; does not persist data).
// This example treats all keys equally and ignores the isId parameter
// See "Complete StorageManager implementation with IndexedDB" below for an
// example where we properly handle isId
class InMemoryStorageManager implements StorageManager {
  private storage = new Map<string, string>();

  async store(key: string, value: string, isId?: boolean): Promise<void> {
    this.storage.set(key, value);
  }

  async retrieve(key: string, isId?: boolean): Promise<string | null> {
    return this.storage.get(key) ?? null;
  }

  async remove(key: string, isId?: boolean): Promise<void> {
    this.storage.delete(key);
  }

  async clearData(storageKeys: string[]): Promise<void> {
    for (const key of storageKeys) {
      this.storage.delete(key);
    }
  }
}

const storageManager: StorageManager = new InMemoryStorageManager();

// Provide device information (use your platform's APIs for non-browser)
const deviceInfo: DeviceInfo = {
  os: 'my-runtime-os',
  language: 'en',
  timezone: 'UTC',
  browser: 'Chrome', // Optional
  browserVersion: '120', // Optional
  userAgent: "some-user-agent" // Optional
};

// Browser-only example (uncomment and adapt if you are running in a web browser)
// const deviceInfo: DeviceInfo = {
//   os: navigator.platform || 'Unknown',
//   language: navigator.language || 'en',
//   timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
//   browser: 'Chrome', // Optional
//   browserVersion: '120', // Optional
//   userAgent: navigator.userAgent // Optional
// };

// Initialize SDK
try {
  const initialized = await initialize({
    apiKey: 'YOUR-API-KEY-HERE',
    baseUrl: 'sdk.iad-01.braze.com', // Your Braze SDK endpoint
    options: {
      sdkVersion: '1.0.0',
      enableLogging: true, // Remove in production
      sessionTimeoutInSeconds: 1800, // 30 minutes
      flushIntervalInSeconds: 10
    },
    sdkMetadata: ['npm'], // Identify your platform
    deviceInfo,
    storageManager
  });

  if (!initialized) {
    console.error('Failed to initialize Braze SDK');
    return;
  }

  // Identify user (wait for promise to resolve)
  await changeUser('user-123');

  // Open session (must be after changeUser)
  const isNewSession = await openSession();
  console.log('Session opened:', isNewSession ? 'new' : 'resumed');

  // Log events
  await logCustomEvent('app_opened', {
    source: 'homepage',
    timestamp: new Date().toISOString()
  });

} catch (error) {
  console.error('Braze SDK error:', error);
}

Implementação de armazenamento personalizado

Implementação completa do StorageManager com IndexedDB para IDs persistentes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import type { StorageManager } from '@braze/javascript-sdk';

class IndexedDBStorageManager implements StorageManager {
  private dbName = 'braze-storage';
  private storeName = 'braze-ids';
  private memoryCache = new Map<string, string>();
  private db: IDBDatabase | null = null;
  private dbInitPromise: Promise<void> | null = null;

  private async initDB(): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName);
        }
      };
    });
  }

  private async ensureDB(): Promise<void> {
    if (this.dbInitPromise !== null) {
      return this.dbInitPromise;
    }
    this.dbInitPromise = this.initDB();
    return this.dbInitPromise;
  }

  async store(key: string, value: string, isId?: boolean): Promise<void> {
    await this.ensureDB();
    this.memoryCache.set(key, value);

    if (isId && this.db) {
      try {
        const transaction = this.db.transaction([this.storeName], 'readwrite');
        const store = transaction.objectStore(this.storeName);
        await new Promise<void>((resolve, reject) => {
          const request = store.put(value, key);
          request.onsuccess = () => resolve();
          request.onerror = () => reject(request.error);
        });
      } catch (error) {
        console.error('Failed to store ID in IndexedDB:', error);
      }
    }
  }

  async retrieve(key: string, isId?: boolean): Promise<string | null> {
    await this.ensureDB();
    if (this.memoryCache.has(key)) {
      return this.memoryCache.get(key) || null;
    }

    if (isId && this.db) {
      try {
        const transaction = this.db.transaction([this.storeName], 'readonly');
        const store = transaction.objectStore(this.storeName);
        return new Promise<string | null>((resolve, reject) => {
          const request = store.get(key);
          request.onsuccess = () => {
            const value = request.result;
            if (value) {
              this.memoryCache.set(key, value);
            }
            resolve(value || null);
          };
          request.onerror = () => reject(request.error);
        });
      } catch (error) {
        console.error('Failed to retrieve ID from IndexedDB:', error);
        return null;
      }
    }

    return null;
  }

  async remove(key: string, isId?: boolean): Promise<void> {
    await this.ensureDB();
    this.memoryCache.delete(key);

    if (isId && this.db) {
      try {
        const transaction = this.db.transaction([this.storeName], 'readwrite');
        const store = transaction.objectStore(this.storeName);
        await new Promise<void>((resolve, reject) => {
          const request = store.delete(key);
          request.onsuccess = () => resolve();
          request.onerror = () => reject(request.error);
        });
      } catch (error) {
        console.error('Failed to remove ID from IndexedDB:', error);
      }
    }
  }

  async clearData(storageKeys: string[]): Promise<void> {
    await this.ensureDB();
    for (const key of storageKeys) {
      this.memoryCache.delete(key);
    }

    if (this.db) {
      try {
        const transaction = this.db.transaction([this.storeName], 'readwrite');
        const store = transaction.objectStore(this.storeName);
        await Promise.all(
          storageKeys.map(
            (key) =>
              new Promise<void>((resolve, reject) => {
                const request = store.delete(key);
                request.onsuccess = () => resolve();
                request.onerror = () => reject(request.error);
              })
          )
        );
      } catch (error) {
        console.error('Failed to clear data from IndexedDB:', error);
      }
    }
  }
}

const storageManager = new IndexedDBStorageManager();

Implementação de rede personalizada

NetworkManager que registra cada requisição de saída (o SDK já lida com erros e retries):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import type { NetworkManager } from '@braze/javascript-sdk';

function logRequest(url: string, data: Partial<Record<string, unknown>>): void {
  // Send to your analytics, monitoring, or logging backend
  console.log('Braze SDK request', { url, data });
}

class LoggingNetworkManager implements NetworkManager {
  async postRequest(
    url: string,
    data: Partial<Record<string, unknown>>,
    headers?: Headers | [string, string][]
  ): Promise<Partial<Record<string, unknown>>> {
    logRequest(url, data);

    const requestHeaders = new Headers(headers);
    requestHeaders.set('Content-Type', 'application/json');

    const response = await fetch(url, {
      method: 'POST',
      headers: requestHeaders,
      body: JSON.stringify(data),
    });

    const result = await response.json();
    return result as Partial<Record<string, unknown>>;
  }
}

const networkManager = new LoggingNetworkManager();

Tratamento de erros

Padrões completos de tratamento de erros:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import {
  getUserId,
  logCustomEvent,
  initialize,
} from '@braze/javascript-sdk';

// Pattern 1: Check for undefined (SDK not initialized)
async function getDevice() {
  const deviceId = await getDeviceId();
  if (deviceId === undefined) {
    console.warn('SDK not initialized');
    return null;
  }
  return deviceId;
}

// Pattern 2: Try-catch for methods that may throw
async function logEventSafely() {
  try {
    const success = await logCustomEvent('button_clicked', { button: 'submit' });
    if (success === undefined) {
      console.warn('SDK not initialized, event not logged');
    } else if (success) {
      console.log('Event logged successfully');
    } else {
      console.warn('Event failed to enqueue');
    }
  } catch (error) {
    console.error('Error logging event:', error);
    // Handle error (e.g., retry, queue for later)
  }
}

// Pattern 3: Handle null vs undefined distinction
async function checkUser() {
  const userId = await getUserId();

  if (userId === undefined) {
    // SDK not initialized
    console.warn('SDK not initialized');
  } else if (userId === null) {
    // Current user is anonymous
    console.log('Current user is anonymous');
  } else {
    // User is identified
    console.log(`User ID is ${userId}`);
  }
}

// Pattern 4: Handle initialization errors
async function initializeSafely() {
  try {
    const initialized = await initialize({
      apiKey: 'YOUR-API-KEY',
      baseUrl: 'sdk.iad-01.braze.com',
      options: { sdkVersion: '1.0.0' },
      sdkMetadata: ['npm'],
      deviceInfo: { os: 'iOS', language: 'en', timezone: 'UTC' },
      storageManager: myStorageManager
    });

    if (!initialized) {
      console.error('Failed to initialize SDK');
      // Check if already initialized, disabled, or validation failed
      return false;
    }

    return true;
  } catch (error) {
    console.error('Initialization error:', error);
    return false;
  }
}

Gerenciamento de inscrições

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {
  ControlMessage,
  logInAppMessageImpression,
  removeSubscription,
  subscribeToInAppMessage,
} from '@braze/javascript-sdk';

const displayMessage = (inAppMessage) => {
  // Add custom code to display in-app messages
}

// Subscribe to in-app messages
const subscriptionId = subscribeToInAppMessage((inAppMessage) => {
  if (inAppMessage instanceof ControlMessage) {
    return; // Skip control messages
  }

  displayMessage(inAppMessage);

  logInAppMessageImpression(inAppMessage);
});

// Later, remove subscription if it was successfully created
if (subscriptionId) {
  removeSubscription(subscriptionId);
}

Trocando a configuração: Apenas uma sessão ativa existe por vez. Para trocar configurações, chame destroy() e depois initialize():

1
2
3
4
import { destroy, initialize } from '@braze/javascript-sdk';

destroy();
await initialize({ /* new config */ });

Casos de uso comuns

Identificação de usuário e rastreamento de atributos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {
  changeUser,
  setCustomUserAttribute,
  setUserEmail,
  setUserFirstName,
  setUserLastName,
} from '@braze/javascript-sdk';

// Identify user
await changeUser('user-123');

// Set standard attributes
await setUserEmail('[email protected]');
await setUserFirstName('John');
await setUserLastName('Doe');

// Set custom attributes
await setCustomUserAttribute('subscription_tier', 'premium');
await setCustomUserAttribute('last_login', new Date());
await setCustomUserAttribute('tags', ['vip', 'early-adopter']);

Registro de eventos e análise de dados

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {
  logCustomEvent,
  logPurchase,
  requestImmediateDataFlush,
} from '@braze/javascript-sdk';

await logCustomEvent('product_viewed', {
  product_id: '123',
  category: 'electronics',
  price: 99.99
});

await logPurchase('product-123', 99.99, 'USD', 1, {
  category: 'electronics'
});

// Flushing these events to the server will happen periodically,
// however you can manually trigger a flush if necessary
requestImmediateDataFlush((success) => {
  console.log('Data flushed:', success);
});

Tratamento de mensagens no app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {
  ControlMessage,
  logInAppMessageImpression,
  subscribeToInAppMessage,
} from '@braze/javascript-sdk';

const displayInAppMessage = async (inAppMessage) => {
  // Add custom code to display in-app messages
}

subscribeToInAppMessage(async (inAppMessage) => {
  if (inAppMessage instanceof ControlMessage) {
    return;
  }

  await displayInAppMessage(inAppMessage);

  await logInAppMessageImpression(inAppMessage);
});

Tratamento de erros e casos extremos

Condições de erro comuns

SDK não inicializado:

  • A maioria dos métodos retorna undefined (não lança exceção) quando o SDK não está inicializado
  • initialize() retorna false se já estiver inicializado ou se a validação falhar
  • changeUser() é um no-op e a promise é resolvida se o SDK não estiver inicializado
  • Sempre verifique se os valores de retorno são undefined antes de usá-los

Falhas de validação:

  • Chave de API ou URL base inválida: initialize() retorna false, registra erro
  • Nomes de eventos/chaves inválidos: Devem ter no máximo 255 caracteres, não podem começar com $, apenas alfanuméricos + pontuação
  • Valores de atributos inválidos: Strings com no máximo 255 caracteres, sem quebras de linha/tabulações/aspas duplas, não podem começar com $
  • Códigos de moeda inválidos: Códigos não suportados resultam em aviso, nenhuma ação é tomada
  • Quantidade de compra inválida: Deve ser de 1 a 100, caso contrário é ignorada

Erros de rede:

  • O postRequest() do NetworkManager deve tratar erros e rejeitar promises adequadamente
  • O controlador de envio de dados faz retry automaticamente em requisições com falha
  • Use o retorno de chamada de requestImmediateDataFlush() para detectar falhas de envio

Erros de armazenamento:

  • Os métodos do StorageManager devem tratar erros de forma adequada
  • Se o armazenamento falhar, o SDK pode não funcionar corretamente
  • A flag isId determina a persistência: IDs persistem entre sessões, objetos têm escopo de sessão

Casos extremos de identificação de usuário:

  • Não é possível reverter para usuário anônimo após a identificação
  • A troca de usuário encerra a sessão atual e inicia uma nova sessão
  • O histórico do usuário anônimo é preservado ao identificar pela primeira vez
  • O histórico é mesclado se o usuário existir em outro dispositivo

Gerenciamento de sessão:

  • As sessões expiram após 30 minutos de inatividade (configurável)
  • openSession() retorna true para nova sessão, false se retomada
  • Deve chamar openSession() após changeUser() ou setIdentifierToken()

Gerenciamento de inscrições:

  • Os retornos de chamada de inscrição são chamados de forma síncrona quando os eventos ocorrem
  • Remova inscrições para evitar vazamentos de memória
  • removeAllSubscriptions() limpa todas as inscrições de uma vez

Envio de dados:

  • Envio automático a cada 10 segundos (configurável, mínimo: 3 segundos)
  • O envio pode falhar silenciosamente - use o retorno de chamada de requestImmediateDataFlush()
  • Os dados são enfileirados se a rede estiver indisponível e enviados quando a rede for restaurada

Notas importantes de implementação

  1. A maioria dos métodos é assíncrona: Métodos assíncronos do SDK retornam uma Promise (use await ou .then()). Alguns métodos de configuração e utilitários (por exemplo, destroy, toggleLogging, setLogger) são síncronos; consulte as definições TypeScript ou a tabela de referência rápida para detalhes.

  2. Métodos podem retornar undefined: Se o SDK não estiver inicializado, a maioria dos métodos retorna undefined em vez de lançar exceção. Verifique se o valor é undefined antes de usar os valores de retorno.

  3. Métodos podem retornar null: Alguns métodos retornam null para indicar “não encontrado” (por exemplo, getUserId() retorna null se o usuário for anônimo). Isso é diferente de undefined (SDK não inicializado).

  4. Chaves de armazenamento usam a flag isId: O parâmetro isId nos métodos do StorageManager distingue entre:
    • Armazenamento de ID: Identificadores persistentes (ID do dispositivo, ID do usuário) que devem persistir entre sessões
    • Armazenamento de objetos: Dados com escopo de sessão que podem ser limpos
  5. Tags de metadados do SDK: O array sdkMetadata identifica a plataforma/wrapper que está usando o SDK (por exemplo, ['npm'] ou [BrazeSdkMetadata.NPM]). Tags válidas são definidas pelo enum BrazeSdkMetadata (como npm, cdn, manu, shp, gg, kep), e o SDK adiciona automaticamente 'wjs' para indicar JavaScript SDK.

  6. NetworkManager padrão: Se networkManager não for fornecido, o SDK usa uma implementação padrão que requer as APIs globais fetch e URL. Forneça uma implementação personalizada se essas APIs não estiverem disponíveis.

  7. PushManager é opcional: Implemente o PushManager apenas se você precisar de funcionalidade de notificação por push. Caso contrário, pode ser omitido.

  8. Destroy e limpeza: Chame destroy() quando precisar encerrar o SDK. Apenas uma sessão ativa pode existir por vez; você deve chamar destroy() antes de chamar initialize() novamente. Isso interrompe timers, envia dados pendentes e libera recursos.

  9. Envio de dados: Os dados são enviados automaticamente a cada 10 segundos (configurável). Use requestImmediateDataFlush() para sincronização imediata.

  10. Gerenciamento de sessão: Sempre chame openSession() após changeUser() ou setIdentifierToken() para evitar a criação de usuários anônimos duplicados.

  11. Segurança de tipos: O SDK é escrito em TypeScript com definições de tipos completas. Use TypeScript para a melhor experiência e verificação de tipos.

  12. Regras de validação: Nomes de eventos, chaves de atributos e chaves de propriedades têm validação rigorosa (máximo de 255 caracteres, não podem começar com $, apenas alfanuméricos + pontuação). Valores inválidos podem ser ignorados ou causar erros.

Depuração / Solução de problemas

Passe a opção enableLogging: true nas opções de inicialização. Isso é útil para desenvolvimento, mas certifique-se de remover essa opção ou fornecer um logger alternativo antes de publicar sua página em produção.

Fale com a gente

Se você tiver dúvidas, entre em contato com [email protected].

Para detalhes do repositório e projetos de exemplo, consulte https://github.com/braze-inc/braze-javascript-sdk.

New Stuff!